@code-pushup/utils 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/index.js +1187 -1237
  2. package/package.json +10 -35
  3. package/src/index.d.ts +13 -11
  4. package/src/lib/errors.d.ts +1 -0
  5. package/src/lib/execute-process.d.ts +5 -3
  6. package/src/lib/git/git.commits-and-tags.d.ts +2 -2
  7. package/src/lib/git/git.d.ts +1 -1
  8. package/src/lib/merge-configs.d.ts +1 -1
  9. package/src/lib/progress.d.ts +1 -1
  10. package/src/lib/reports/constants.d.ts +2 -27
  11. package/src/lib/reports/flatten-plugins.d.ts +1 -1
  12. package/src/lib/reports/formatting.d.ts +5 -5
  13. package/src/lib/reports/generate-md-report-categoy-section.d.ts +8 -6
  14. package/src/lib/reports/generate-md-report.d.ts +10 -9
  15. package/src/lib/reports/generate-md-reports-diff-utils.d.ts +29 -0
  16. package/src/lib/reports/generate-md-reports-diff.d.ts +4 -1
  17. package/src/lib/reports/load-report.d.ts +6 -0
  18. package/src/lib/reports/log-stdout-summary.d.ts +3 -1
  19. package/src/lib/reports/scoring.d.ts +2 -2
  20. package/src/lib/reports/sorting.d.ts +5 -1
  21. package/src/lib/reports/types.d.ts +1 -1
  22. package/src/lib/reports/utils.d.ts +19 -13
  23. package/src/lib/text-formats/constants.d.ts +8 -0
  24. package/src/lib/text-formats/html/table.d.ts +1 -1
  25. package/src/lib/text-formats/index.d.ts +9 -40
  26. package/src/lib/text-formats/table.d.ts +1 -1
  27. package/src/lib/transform.d.ts +0 -1
  28. package/src/lib/types.d.ts +9 -0
  29. package/src/lib/text-formats/md/font-style.d.ts +0 -4
  30. package/src/lib/text-formats/md/headline.d.ts +0 -14
  31. package/src/lib/text-formats/md/image.d.ts +0 -1
  32. package/src/lib/text-formats/md/link.d.ts +0 -1
  33. package/src/lib/text-formats/md/list.d.ts +0 -7
  34. package/src/lib/text-formats/md/paragraphs.d.ts +0 -1
  35. package/src/lib/text-formats/md/section.d.ts +0 -2
  36. package/src/lib/text-formats/md/table.d.ts +0 -9
  37. package/src/lib/text-formats/types.d.ts +0 -1
package/index.js CHANGED
@@ -666,6 +666,8 @@ var auditResultSchema = scorableWithPluginMetaSchema.merge(
666
666
  );
667
667
  var reportsDiffSchema = z15.object({
668
668
  commits: makeComparisonSchema(commitSchema).nullable().describe("Commits identifying compared reports"),
669
+ portalUrl: urlSchema.optional().describe("Link to comparison page in Code PushUp portal"),
670
+ label: z15.string().optional().describe("Label (e.g. project name)"),
669
671
  categories: makeArraysComparisonSchema(
670
672
  categoryDiffSchema,
671
673
  categoryResultSchema,
@@ -735,15 +737,286 @@ function comparePairs(pairs, equalsFn) {
735
737
  );
736
738
  }
737
739
 
740
+ // packages/utils/src/lib/errors.ts
741
+ function stringifyError(error) {
742
+ if (error instanceof Error) {
743
+ if (error.name === "Error" || error.message.startsWith(error.name)) {
744
+ return error.message;
745
+ }
746
+ return `${error.name}: ${error.message}`;
747
+ }
748
+ if (typeof error === "string") {
749
+ return error;
750
+ }
751
+ return JSON.stringify(error);
752
+ }
753
+
738
754
  // packages/utils/src/lib/execute-process.ts
739
- import { spawn } from "node:child_process";
755
+ import {
756
+ spawn
757
+ } from "node:child_process";
758
+
759
+ // packages/utils/src/lib/reports/utils.ts
760
+ import ansis from "ansis";
761
+ import { md } from "build-md";
762
+
763
+ // packages/utils/src/lib/reports/constants.ts
764
+ var TERMINAL_WIDTH = 80;
765
+ var SCORE_COLOR_RANGE = {
766
+ GREEN_MIN: 0.9,
767
+ YELLOW_MIN: 0.5
768
+ };
769
+ var FOOTER_PREFIX = "Made with \u2764 by";
770
+ var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
771
+ var README_LINK = "https://github.com/code-pushup/cli#readme";
772
+ var REPORT_HEADLINE_TEXT = "Code PushUp Report";
773
+ var REPORT_RAW_OVERVIEW_TABLE_HEADERS = [
774
+ "Category",
775
+ "Score",
776
+ "Audits"
777
+ ];
740
778
 
741
779
  // packages/utils/src/lib/reports/utils.ts
742
- import { join as join2 } from "node:path";
780
+ function formatReportScore(score) {
781
+ const scaledScore = score * 100;
782
+ const roundedScore = Math.round(scaledScore);
783
+ return roundedScore === 100 && score !== 1 ? Math.floor(scaledScore).toString() : roundedScore.toString();
784
+ }
785
+ function formatScoreWithColor(score, options) {
786
+ const styledNumber = options?.skipBold ? formatReportScore(score) : md.bold(formatReportScore(score));
787
+ return md`${scoreMarker(score)} ${styledNumber}`;
788
+ }
789
+ var MARKERS = {
790
+ circle: {
791
+ red: "\u{1F534}",
792
+ yellow: "\u{1F7E1}",
793
+ green: "\u{1F7E2}"
794
+ },
795
+ square: {
796
+ red: "\u{1F7E5}",
797
+ yellow: "\u{1F7E8}",
798
+ green: "\u{1F7E9}"
799
+ }
800
+ };
801
+ function scoreMarker(score, markerType = "circle") {
802
+ if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
803
+ return MARKERS[markerType].green;
804
+ }
805
+ if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
806
+ return MARKERS[markerType].yellow;
807
+ }
808
+ return MARKERS[markerType].red;
809
+ }
810
+ function getDiffMarker(diff) {
811
+ if (diff > 0) {
812
+ return "\u2191";
813
+ }
814
+ if (diff < 0) {
815
+ return "\u2193";
816
+ }
817
+ return "";
818
+ }
819
+ function colorByScoreDiff(text, diff) {
820
+ const color = diff > 0 ? "green" : diff < 0 ? "red" : "gray";
821
+ return shieldsBadge(text, color);
822
+ }
823
+ function shieldsBadge(text, color) {
824
+ return md.image(
825
+ `https://img.shields.io/badge/${encodeURIComponent(text)}-${color}`,
826
+ text
827
+ );
828
+ }
829
+ function formatDiffNumber(diff) {
830
+ const number = Math.abs(diff) === Number.POSITIVE_INFINITY ? "\u221E" : `${Math.abs(diff)}`;
831
+ const sign = diff < 0 ? "\u2212" : "+";
832
+ return `${sign}${number}`;
833
+ }
834
+ function severityMarker(severity) {
835
+ if (severity === "error") {
836
+ return "\u{1F6A8}";
837
+ }
838
+ if (severity === "warning") {
839
+ return "\u26A0\uFE0F";
840
+ }
841
+ return "\u2139\uFE0F";
842
+ }
843
+ function formatScoreChange(diff) {
844
+ const marker = getDiffMarker(diff);
845
+ const text = formatDiffNumber(Math.round(diff * 1e3) / 10);
846
+ return colorByScoreDiff(`${marker} ${text}`, diff);
847
+ }
848
+ function formatValueChange({
849
+ values,
850
+ scores
851
+ }) {
852
+ const marker = getDiffMarker(values.diff);
853
+ const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : Math.round(100 * values.diff / values.before);
854
+ const text = `${formatDiffNumber(percentage)}\u2009%`;
855
+ return colorByScoreDiff(`${marker} ${text}`, scores.diff);
856
+ }
857
+ function calcDuration(start, stop) {
858
+ return Math.round((stop ?? performance.now()) - start);
859
+ }
860
+ function countCategoryAudits(refs, plugins) {
861
+ const groupLookup = plugins.reduce(
862
+ (lookup, plugin) => {
863
+ if (plugin.groups == null || plugin.groups.length === 0) {
864
+ return lookup;
865
+ }
866
+ return {
867
+ ...lookup,
868
+ [plugin.slug]: Object.fromEntries(
869
+ plugin.groups.map((group) => [group.slug, group])
870
+ )
871
+ };
872
+ },
873
+ {}
874
+ );
875
+ return refs.reduce((acc, ref) => {
876
+ if (ref.type === "group") {
877
+ const groupRefs = groupLookup[ref.plugin]?.[ref.slug]?.refs;
878
+ return acc + (groupRefs?.length ?? 0);
879
+ }
880
+ return acc + 1;
881
+ }, 0);
882
+ }
883
+ function compareCategoryAuditsAndGroups(a, b) {
884
+ if (a.score !== b.score) {
885
+ return a.score - b.score;
886
+ }
887
+ if (a.weight !== b.weight) {
888
+ return b.weight - a.weight;
889
+ }
890
+ if ("value" in a && "value" in b && a.value !== b.value) {
891
+ return b.value - a.value;
892
+ }
893
+ return a.title.localeCompare(b.title);
894
+ }
895
+ function compareAudits(a, b) {
896
+ if (a.score !== b.score) {
897
+ return a.score - b.score;
898
+ }
899
+ if (a.value !== b.value) {
900
+ return b.value - a.value;
901
+ }
902
+ return a.title.localeCompare(b.title);
903
+ }
904
+ function compareIssueSeverity(severity1, severity2) {
905
+ const levels = {
906
+ info: 0,
907
+ warning: 1,
908
+ error: 2
909
+ };
910
+ return levels[severity1] - levels[severity2];
911
+ }
912
+ function throwIsNotPresentError(itemName, presentPlace) {
913
+ throw new Error(`${itemName} is not present in ${presentPlace}`);
914
+ }
915
+ function getPluginNameFromSlug(slug, plugins) {
916
+ return plugins.find(({ slug: pluginSlug }) => pluginSlug === slug)?.title || slug;
917
+ }
918
+ function compareIssues(a, b) {
919
+ if (a.severity !== b.severity) {
920
+ return -compareIssueSeverity(a.severity, b.severity);
921
+ }
922
+ if (!a.source && b.source) {
923
+ return -1;
924
+ }
925
+ if (a.source && !b.source) {
926
+ return 1;
927
+ }
928
+ if (a.source?.file !== b.source?.file) {
929
+ return a.source?.file.localeCompare(b.source?.file || "") ?? 0;
930
+ }
931
+ if (!a.source?.position && b.source?.position) {
932
+ return -1;
933
+ }
934
+ if (a.source?.position && !b.source?.position) {
935
+ return 1;
936
+ }
937
+ if (a.source?.position?.startLine !== b.source?.position?.startLine) {
938
+ return (a.source?.position?.startLine ?? 0) - (b.source?.position?.startLine ?? 0);
939
+ }
940
+ return 0;
941
+ }
942
+ function applyScoreColor({ score, text }, style = ansis) {
943
+ const formattedScore = text ?? formatReportScore(score);
944
+ if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
945
+ return text ? style.green(formattedScore) : style.bold(style.green(formattedScore));
946
+ }
947
+ if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
948
+ return text ? style.yellow(formattedScore) : style.bold(style.yellow(formattedScore));
949
+ }
950
+ return text ? style.red(formattedScore) : style.bold(style.red(formattedScore));
951
+ }
952
+ function targetScoreIcon(score, targetScore, options = {}) {
953
+ if (targetScore != null) {
954
+ const {
955
+ passIcon = "\u2705",
956
+ failIcon = "\u274C",
957
+ prefix = "",
958
+ postfix = ""
959
+ } = options;
960
+ if (score >= targetScore) {
961
+ return `${prefix}${passIcon}${postfix}`;
962
+ }
963
+ return `${prefix}${failIcon}${postfix}`;
964
+ }
965
+ return "";
966
+ }
967
+
968
+ // packages/utils/src/lib/execute-process.ts
969
+ var ProcessError = class extends Error {
970
+ code;
971
+ stderr;
972
+ stdout;
973
+ constructor(result) {
974
+ super(result.stderr);
975
+ this.code = result.code;
976
+ this.stderr = result.stderr;
977
+ this.stdout = result.stdout;
978
+ }
979
+ };
980
+ function executeProcess(cfg) {
981
+ const { command, args, observer, ignoreExitCode = false, ...options } = cfg;
982
+ const { onStdout, onStderr, onError, onComplete } = observer ?? {};
983
+ const date = (/* @__PURE__ */ new Date()).toISOString();
984
+ const start = performance.now();
985
+ return new Promise((resolve, reject) => {
986
+ const spawnedProcess = spawn(command, args ?? [], {
987
+ shell: true,
988
+ ...options
989
+ });
990
+ let stdout = "";
991
+ let stderr = "";
992
+ spawnedProcess.stdout.on("data", (data) => {
993
+ stdout += String(data);
994
+ onStdout?.(String(data), spawnedProcess);
995
+ });
996
+ spawnedProcess.stderr.on("data", (data) => {
997
+ stderr += String(data);
998
+ onStderr?.(String(data), spawnedProcess);
999
+ });
1000
+ spawnedProcess.on("error", (err) => {
1001
+ stderr += err.toString();
1002
+ });
1003
+ spawnedProcess.on("close", (code2) => {
1004
+ const timings = { date, duration: calcDuration(start) };
1005
+ if (code2 === 0 || ignoreExitCode) {
1006
+ onComplete?.();
1007
+ resolve({ code: code2, stdout, stderr, ...timings });
1008
+ } else {
1009
+ const errorMsg = new ProcessError({ code: code2, stdout, stderr, ...timings });
1010
+ onError?.(errorMsg);
1011
+ reject(errorMsg);
1012
+ }
1013
+ });
1014
+ });
1015
+ }
743
1016
 
744
1017
  // packages/utils/src/lib/file-system.ts
1018
+ import { bold, gray } from "ansis";
745
1019
  import { bundleRequire } from "bundle-require";
746
- import chalk2 from "chalk";
747
1020
  import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
748
1021
  import { join } from "node:path";
749
1022
 
@@ -836,55 +1109,7 @@ function isPromiseRejectedResult(result) {
836
1109
  // packages/utils/src/lib/logging.ts
837
1110
  import isaacs_cliui from "@isaacs/cliui";
838
1111
  import { cliui } from "@poppinss/cliui";
839
- import chalk from "chalk";
840
-
841
- // packages/utils/src/lib/reports/constants.ts
842
- var TERMINAL_WIDTH = 80;
843
- var SCORE_COLOR_RANGE = {
844
- GREEN_MIN: 0.9,
845
- YELLOW_MIN: 0.5
846
- };
847
- var CATEGORIES_TITLE = "\u{1F3F7} Categories";
848
- var FOOTER_PREFIX = "Made with \u2764 by";
849
- var CODE_PUSHUP_DOMAIN = "code-pushup.dev";
850
- var README_LINK = "https://github.com/code-pushup/cli#readme";
851
- var reportHeadlineText = "Code PushUp Report";
852
- var reportOverviewTableHeaders = [
853
- {
854
- key: "category",
855
- label: "\u{1F3F7} Category",
856
- align: "left"
857
- },
858
- {
859
- key: "score",
860
- label: "\u2B50 Score"
861
- },
862
- {
863
- key: "audits",
864
- label: "\u{1F6E1} Audits"
865
- }
866
- ];
867
- var reportRawOverviewTableHeaders = ["Category", "Score", "Audits"];
868
- var issuesTableHeadings = [
869
- {
870
- key: "severity",
871
- label: "Severity"
872
- },
873
- {
874
- key: "message",
875
- label: "Message"
876
- },
877
- {
878
- key: "file",
879
- label: "Source file"
880
- },
881
- {
882
- key: "line",
883
- label: "Line(s)"
884
- }
885
- ];
886
-
887
- // packages/utils/src/lib/logging.ts
1112
+ import { underline } from "ansis";
888
1113
  var singletonUiInstance;
889
1114
  function ui() {
890
1115
  if (singletonUiInstance === void 0) {
@@ -908,7 +1133,7 @@ function logListItem(args) {
908
1133
  singletonUiInstance?.logger.log(content);
909
1134
  }
910
1135
  function link(text) {
911
- return chalk.underline(chalk.blueBright(text));
1136
+ return underline.blueBright(text);
912
1137
  }
913
1138
 
914
1139
  // packages/utils/src/lib/log-results.ts
@@ -988,10 +1213,10 @@ async function removeDirectoryIfExists(dir) {
988
1213
  function logMultipleFileResults(fileResults, messagePrefix) {
989
1214
  const succeededTransform = (result) => {
990
1215
  const [fileName, size] = result.value;
991
- const formattedSize = size ? ` (${chalk2.gray(formatBytes(size))})` : "";
992
- return `- ${chalk2.bold(fileName)}${formattedSize}`;
1216
+ const formattedSize = size ? ` (${gray(formatBytes(size))})` : "";
1217
+ return `- ${bold(fileName)}${formattedSize}`;
993
1218
  };
994
- const failedTransform = (result) => `- ${chalk2.bold(result.reason)}`;
1219
+ const failedTransform = (result) => `- ${bold(result.reason)}`;
995
1220
  logMultipleResults(
996
1221
  fileResults,
997
1222
  messagePrefix,
@@ -1031,46 +1256,25 @@ async function crawlFileSystem(options) {
1031
1256
  return resultsNestedArray.flat();
1032
1257
  }
1033
1258
  function findLineNumberInText(content, pattern) {
1034
- const lines6 = content.split(/\r?\n/);
1035
- const lineNumber = lines6.findIndex((line) => line.includes(pattern)) + 1;
1259
+ const lines = content.split(/\r?\n/);
1260
+ const lineNumber = lines.findIndex((line) => line.includes(pattern)) + 1;
1036
1261
  return lineNumber === 0 ? null : lineNumber;
1037
1262
  }
1038
1263
  function filePathToCliArg(path) {
1039
1264
  return `"${path}"`;
1040
1265
  }
1041
1266
 
1042
- // packages/utils/src/lib/text-formats/constants.ts
1043
- var NEW_LINE = "\n";
1044
- var TAB = " ";
1045
- var SPACE = " ";
1046
-
1047
- // packages/utils/src/lib/text-formats/html/details.ts
1048
- function details(title, content, cfg = { open: false }) {
1049
- return `<details${cfg.open ? " open" : ""}>${NEW_LINE}<summary>${title}</summary>${NEW_LINE}${// ⚠️ The blank line is needed to ensure Markdown in content is rendered correctly.
1050
- NEW_LINE}${content}${NEW_LINE}${// @TODO in the future we could consider adding it only if the content ends with a code block
1051
- // ⚠️ The blank line ensure Markdown in content is rendered correctly.
1052
- NEW_LINE}</details>${// ⚠️ The blank line is needed to ensure Markdown after details is rendered correctly.
1053
- NEW_LINE}`;
1054
- }
1055
-
1056
- // packages/utils/src/lib/text-formats/html/font-style.ts
1057
- var boldElement = "b";
1058
- function bold(text) {
1059
- return `<${boldElement}>${text}</${boldElement}>`;
1060
- }
1061
- var italicElement = "i";
1062
- function italic(text) {
1063
- return `<${italicElement}>${text}</${italicElement}>`;
1064
- }
1065
- var codeElement = "code";
1066
- function code(text) {
1067
- return `<${codeElement}>${text}</${codeElement}>`;
1267
+ // packages/utils/src/lib/filter.ts
1268
+ function filterItemRefsBy(items, refFilterFn) {
1269
+ return items.map((item) => ({
1270
+ ...item,
1271
+ refs: item.refs.filter(refFilterFn)
1272
+ })).filter((item) => item.refs.length);
1068
1273
  }
1069
1274
 
1070
- // packages/utils/src/lib/text-formats/html/link.ts
1071
- function link2(href, text) {
1072
- return `<a href="${href}">${text || href}</a>`;
1073
- }
1275
+ // packages/utils/src/lib/git/git.ts
1276
+ import { isAbsolute, join as join2, relative } from "node:path";
1277
+ import { simpleGit } from "simple-git";
1074
1278
 
1075
1279
  // packages/utils/src/lib/transform.ts
1076
1280
  import { platform } from "node:os";
@@ -1121,6 +1325,12 @@ function objectToCliArgs(params) {
1121
1325
  if (Array.isArray(value)) {
1122
1326
  return value.map((v) => `${prefix}${key}="${v}"`);
1123
1327
  }
1328
+ if (typeof value === "object") {
1329
+ return Object.entries(value).flatMap(
1330
+ // transform nested objects to the dot notation `key.subkey`
1331
+ ([k, v]) => objectToCliArgs({ [`${key}.${k}`]: v })
1332
+ );
1333
+ }
1124
1334
  if (typeof value === "string") {
1125
1335
  return [`${prefix}${key}="${value}"`];
1126
1336
  }
@@ -1151,11 +1361,6 @@ function capitalize(text) {
1151
1361
  1
1152
1362
  )}`;
1153
1363
  }
1154
- function apostrophize(text, upperCase) {
1155
- const lastCharMatch = text.match(/(\w)\W*$/);
1156
- const lastChar = lastCharMatch?.[1] ?? "";
1157
- return `${text}'${lastChar.toLocaleLowerCase() === "s" ? "" : upperCase ? "S" : "s"}`;
1158
- }
1159
1364
  function toNumberPrecision(value, decimalPlaces) {
1160
1365
  return Number(
1161
1366
  `${Math.round(
@@ -1176,643 +1381,126 @@ function toOrdinal(value) {
1176
1381
  return `${value}th`;
1177
1382
  }
1178
1383
 
1179
- // packages/utils/src/lib/text-formats/table.ts
1180
- function rowToStringArray({ rows, columns = [] }) {
1181
- if (Array.isArray(rows.at(0)) && typeof columns.at(0) === "object") {
1182
- throw new TypeError(
1183
- "Column can`t be object when rows are primitive values"
1184
- );
1185
- }
1186
- return rows.map((row) => {
1187
- if (Array.isArray(row)) {
1188
- return row.map(String);
1189
- }
1190
- const objectRow = row;
1191
- if (columns.length === 0 || typeof columns.at(0) === "string") {
1192
- return Object.values(objectRow).map(
1193
- (value) => value == null ? "" : String(value)
1194
- );
1195
- }
1196
- return columns.map(
1197
- ({ key }) => objectRow[key] == null ? "" : String(objectRow[key])
1198
- );
1199
- });
1200
- }
1201
- function columnsToStringArray({
1202
- rows,
1203
- columns = []
1204
- }) {
1205
- const firstRow = rows.at(0);
1206
- const primitiveRows = Array.isArray(firstRow);
1207
- if (typeof columns.at(0) === "string" && !primitiveRows) {
1208
- throw new Error("invalid union type. Caught by model parsing.");
1209
- }
1210
- if (columns.length === 0) {
1211
- if (Array.isArray(firstRow)) {
1212
- return firstRow.map((_, idx) => String(idx));
1213
- }
1214
- return Object.keys(firstRow);
1215
- }
1216
- if (typeof columns.at(0) === "string") {
1217
- return columns.map(String);
1218
- }
1219
- const cols = columns;
1220
- return cols.map(({ label, key }) => label ?? capitalize(key));
1384
+ // packages/utils/src/lib/git/git.ts
1385
+ function getGitRoot(git = simpleGit()) {
1386
+ return git.revparse("--show-toplevel");
1221
1387
  }
1222
- function getColumnAlignmentForKeyAndIndex(targetKey, targetIdx, columns = []) {
1223
- const column = columns.at(targetIdx) ?? columns.find((col) => col.key === targetKey);
1224
- if (typeof column === "string") {
1225
- return column;
1226
- } else if (typeof column === "object") {
1227
- return column.align ?? "center";
1228
- } else {
1229
- return "center";
1230
- }
1388
+ function formatGitPath(path, gitRoot) {
1389
+ const absolutePath = isAbsolute(path) ? path : join2(process.cwd(), path);
1390
+ const relativePath = relative(gitRoot, absolutePath);
1391
+ return toUnixPath(relativePath);
1231
1392
  }
1232
- function getColumnAlignmentForIndex(targetIdx, columns = []) {
1233
- const column = columns.at(targetIdx);
1234
- if (column == null) {
1235
- return "center";
1236
- } else if (typeof column === "string") {
1237
- return column;
1238
- } else if (typeof column === "object") {
1239
- return column.align ?? "center";
1240
- } else {
1241
- return "center";
1242
- }
1393
+ async function toGitPath(path, git = simpleGit()) {
1394
+ const gitRoot = await getGitRoot(git);
1395
+ return formatGitPath(path, gitRoot);
1243
1396
  }
1244
- function getColumnAlignments(tableData) {
1245
- const { rows, columns = [] } = tableData;
1246
- if (rows.at(0) == null) {
1247
- throw new Error("first row can`t be undefined.");
1248
- }
1249
- if (Array.isArray(rows.at(0))) {
1250
- const firstPrimitiveRow = rows.at(0);
1251
- return Array.from({ length: firstPrimitiveRow.length }).map(
1252
- (_, idx) => getColumnAlignmentForIndex(idx, columns)
1397
+ var GitStatusError = class _GitStatusError extends Error {
1398
+ static ignoredProps = /* @__PURE__ */ new Set(["current", "tracking"]);
1399
+ static getReducedStatus(status) {
1400
+ return Object.fromEntries(
1401
+ Object.entries(status).filter(([key]) => !this.ignoredProps.has(key)).filter(
1402
+ (entry) => {
1403
+ const value = entry[1];
1404
+ if (value == null) {
1405
+ return false;
1406
+ }
1407
+ if (Array.isArray(value) && value.length === 0) {
1408
+ return false;
1409
+ }
1410
+ if (typeof value === "number" && value === 0) {
1411
+ return false;
1412
+ }
1413
+ return !(typeof value === "boolean" && !value);
1414
+ }
1415
+ )
1253
1416
  );
1254
1417
  }
1255
- const biggestRow = [...rows].sort((a, b) => Object.keys(a).length - Object.keys(b).length).at(-1);
1256
- if (columns.length > 0) {
1257
- return columns.map(
1258
- (column, idx) => typeof column === "string" ? column : getColumnAlignmentForKeyAndIndex(
1259
- column.key,
1260
- idx,
1261
- columns
1262
- )
1418
+ constructor(status) {
1419
+ super(
1420
+ `Working directory needs to be clean before we you can proceed. Commit your local changes or stash them:
1421
+ ${JSON.stringify(
1422
+ _GitStatusError.getReducedStatus(status),
1423
+ null,
1424
+ 2
1425
+ )}`
1263
1426
  );
1264
1427
  }
1265
- return Object.keys(biggestRow ?? {}).map((_) => "center");
1266
- }
1267
-
1268
- // packages/utils/src/lib/text-formats/html/table.ts
1269
- function wrap(elem, content) {
1270
- return `<${elem}>${content}</${elem}>${NEW_LINE}`;
1271
- }
1272
- function wrapRow(content) {
1273
- const elem = "tr";
1274
- return `<${elem}>${NEW_LINE}${content}</${elem}>${NEW_LINE}`;
1275
- }
1276
- function table(tableData) {
1277
- if (tableData.rows.length === 0) {
1278
- throw new Error("Data can't be empty");
1428
+ };
1429
+ async function guardAgainstLocalChanges(git = simpleGit()) {
1430
+ const status = await git.status(["-s"]);
1431
+ if (status.files.length > 0) {
1432
+ throw new GitStatusError(status);
1279
1433
  }
1280
- const tableHeaderCols = columnsToStringArray(tableData).map((s) => wrap("th", s)).join("");
1281
- const tableHeaderRow = wrapRow(tableHeaderCols);
1282
- const tableBody = rowToStringArray(tableData).map((arr) => {
1283
- const columns = arr.map((s) => wrap("td", s)).join("");
1284
- return wrapRow(columns);
1285
- }).join("");
1286
- return wrap("table", `${NEW_LINE}${tableHeaderRow}${tableBody}`);
1287
- }
1288
-
1289
- // packages/utils/src/lib/text-formats/md/font-style.ts
1290
- var boldWrap = "**";
1291
- function bold2(text) {
1292
- return `${boldWrap}${text}${boldWrap}`;
1293
1434
  }
1294
- var italicWrap = "_";
1295
- function italic2(text) {
1296
- return `${italicWrap}${text}${italicWrap}`;
1297
- }
1298
- var strikeThroughWrap = "~";
1299
- function strikeThrough(text) {
1300
- return `${strikeThroughWrap}${text}${strikeThroughWrap}`;
1301
- }
1302
- var codeWrap = "`";
1303
- function code2(text) {
1304
- return `${codeWrap}${text}${codeWrap}`;
1305
- }
1306
-
1307
- // packages/utils/src/lib/text-formats/md/headline.ts
1308
- function headline(text, hierarchy = 1) {
1309
- return `${"#".repeat(hierarchy)} ${text}${NEW_LINE}`;
1310
- }
1311
- function h(text, hierarchy = 1) {
1312
- return headline(text, hierarchy);
1313
- }
1314
- function h1(text) {
1315
- return headline(text, 1);
1316
- }
1317
- function h2(text) {
1318
- return headline(text, 2);
1319
- }
1320
- function h3(text) {
1321
- return headline(text, 3);
1322
- }
1323
- function h4(text) {
1324
- return headline(text, 4);
1325
- }
1326
- function h5(text) {
1327
- return headline(text, 5);
1328
- }
1329
- function h6(text) {
1330
- return headline(text, 6);
1331
- }
1332
-
1333
- // packages/utils/src/lib/text-formats/md/image.ts
1334
- function image(src, alt) {
1335
- return `![${alt}](${src})`;
1435
+ async function safeCheckout(branchOrHash, forceCleanStatus = false, git = simpleGit()) {
1436
+ if (forceCleanStatus) {
1437
+ await git.raw(["reset", "--hard"]);
1438
+ await git.clean(["f", "d"]);
1439
+ ui().logger.info(`git status cleaned`);
1440
+ }
1441
+ await guardAgainstLocalChanges(git);
1442
+ await git.checkout(branchOrHash);
1336
1443
  }
1337
1444
 
1338
- // packages/utils/src/lib/text-formats/md/link.ts
1339
- function link3(href, text) {
1340
- return `[${text || href}](${href})`;
1341
- }
1445
+ // packages/utils/src/lib/git/git.commits-and-tags.ts
1446
+ import { simpleGit as simpleGit2 } from "simple-git";
1342
1447
 
1343
- // packages/utils/src/lib/text-formats/md/list.ts
1344
- function li(text, order = "unordered") {
1345
- const style = order === "unordered" ? "-" : "- [ ]";
1346
- return `${style} ${text}`;
1448
+ // packages/utils/src/lib/semver.ts
1449
+ import { rcompare, valid } from "semver";
1450
+ function normalizeSemver(semverString) {
1451
+ if (semverString.startsWith("v") || semverString.startsWith("V")) {
1452
+ return semverString.slice(1);
1453
+ }
1454
+ if (semverString.includes("@")) {
1455
+ return semverString.split("@").at(-1) ?? "";
1456
+ }
1457
+ return semverString;
1347
1458
  }
1348
- function indentation(text, level = 1) {
1349
- return `${TAB.repeat(level)}${text}`;
1459
+ function isSemver(semverString = "") {
1460
+ return valid(normalizeSemver(semverString)) != null;
1350
1461
  }
1351
-
1352
- // packages/utils/src/lib/text-formats/md/paragraphs.ts
1353
- function paragraphs(...sections) {
1354
- return sections.filter(Boolean).join(`${NEW_LINE}${NEW_LINE}`);
1462
+ function sortSemvers(semverStrings) {
1463
+ return semverStrings.map(normalizeSemver).filter(isSemver).sort(rcompare);
1355
1464
  }
1356
1465
 
1357
- // packages/utils/src/lib/text-formats/md/section.ts
1358
- function section(...contents) {
1359
- return `${lines(...contents)}${NEW_LINE}`;
1466
+ // packages/utils/src/lib/git/git.commits-and-tags.ts
1467
+ async function getLatestCommit(git = simpleGit2()) {
1468
+ const log2 = await git.log({
1469
+ maxCount: 1,
1470
+ // git log -1 --pretty=format:"%H %s %an %aI" - See: https://git-scm.com/docs/pretty-formats
1471
+ format: { hash: "%H", message: "%s", author: "%an", date: "%aI" }
1472
+ });
1473
+ return commitSchema.parse(log2.latest);
1360
1474
  }
1361
- function lines(...contents) {
1362
- const filteredContent = contents.filter(
1363
- (value) => value != null && value !== "" && value !== false
1364
- );
1365
- return `${filteredContent.join(NEW_LINE)}`;
1475
+ async function getCurrentBranchOrTag(git = simpleGit2()) {
1476
+ return await git.branch().then((r) => r.current) || // If no current branch, try to get the tag
1477
+ // @TODO use simple git
1478
+ await git.raw(["describe", "--tags", "--exact-match"]).then((out) => out.trim());
1366
1479
  }
1367
-
1368
- // packages/utils/src/lib/text-formats/md/table.ts
1369
- var alignString = /* @__PURE__ */ new Map([
1370
- ["left", ":--"],
1371
- ["center", ":--:"],
1372
- ["right", "--:"]
1373
- ]);
1374
- function tableRow(rows) {
1375
- return `|${rows.join("|")}|`;
1480
+ function validateFilter({ from, to }) {
1481
+ if (to && !from) {
1482
+ throw new Error(
1483
+ `filter needs the "from" option defined to accept the "to" option.
1484
+ `
1485
+ );
1486
+ }
1376
1487
  }
1377
- function table2(data) {
1378
- if (data.rows.length === 0) {
1379
- throw new Error("Data can't be empty");
1488
+ function filterLogs(allTags, opt) {
1489
+ if (!opt) {
1490
+ return allTags;
1380
1491
  }
1381
- const alignmentRow = getColumnAlignments(data).map(
1382
- (s) => alignString.get(s) ?? String(alignString.get("center"))
1383
- );
1384
- return section(
1385
- `${lines(
1386
- tableRow(columnsToStringArray(data)),
1387
- tableRow(alignmentRow),
1388
- ...rowToStringArray(data).map(tableRow)
1389
- )}`
1390
- );
1391
- }
1392
-
1393
- // packages/utils/src/lib/text-formats/index.ts
1394
- var md = {
1395
- bold: bold2,
1396
- italic: italic2,
1397
- strikeThrough,
1398
- code: code2,
1399
- link: link3,
1400
- image,
1401
- headline,
1402
- h,
1403
- h1,
1404
- h2,
1405
- h3,
1406
- h4,
1407
- h5,
1408
- h6,
1409
- indentation,
1410
- lines,
1411
- li,
1412
- section,
1413
- paragraphs,
1414
- table: table2
1415
- };
1416
- var html = {
1417
- bold,
1418
- italic,
1419
- code,
1420
- link: link2,
1421
- details,
1422
- table
1423
- };
1424
-
1425
- // packages/utils/src/lib/reports/utils.ts
1426
- var { image: image2, bold: boldMd } = md;
1427
- function formatReportScore(score) {
1428
- const scaledScore = score * 100;
1429
- const roundedScore = Math.round(scaledScore);
1430
- return roundedScore === 100 && score !== 1 ? Math.floor(scaledScore).toString() : roundedScore.toString();
1431
- }
1432
- function formatScoreWithColor(score, options) {
1433
- const styledNumber = options?.skipBold ? formatReportScore(score) : boldMd(formatReportScore(score));
1434
- return `${scoreMarker(score)} ${styledNumber}`;
1435
- }
1436
- var MARKERS = {
1437
- circle: {
1438
- red: "\u{1F534}",
1439
- yellow: "\u{1F7E1}",
1440
- green: "\u{1F7E2}"
1441
- },
1442
- square: {
1443
- red: "\u{1F7E5}",
1444
- yellow: "\u{1F7E8}",
1445
- green: "\u{1F7E9}"
1446
- }
1447
- };
1448
- function scoreMarker(score, markerType = "circle") {
1449
- if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
1450
- return MARKERS[markerType].green;
1451
- }
1452
- if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
1453
- return MARKERS[markerType].yellow;
1454
- }
1455
- return MARKERS[markerType].red;
1456
- }
1457
- function getDiffMarker(diff) {
1458
- if (diff > 0) {
1459
- return "\u2191";
1460
- }
1461
- if (diff < 0) {
1462
- return "\u2193";
1463
- }
1464
- return "";
1465
- }
1466
- function colorByScoreDiff(text, diff) {
1467
- const color = diff > 0 ? "green" : diff < 0 ? "red" : "gray";
1468
- return shieldsBadge(text, color);
1469
- }
1470
- function shieldsBadge(text, color) {
1471
- return image2(
1472
- `https://img.shields.io/badge/${encodeURIComponent(text)}-${color}`,
1473
- text
1474
- );
1475
- }
1476
- function formatDiffNumber(diff) {
1477
- const number = Math.abs(diff) === Number.POSITIVE_INFINITY ? "\u221E" : `${Math.abs(diff)}`;
1478
- const sign = diff < 0 ? "\u2212" : "+";
1479
- return `${sign}${number}`;
1480
- }
1481
- function severityMarker(severity) {
1482
- if (severity === "error") {
1483
- return "\u{1F6A8}";
1484
- }
1485
- if (severity === "warning") {
1486
- return "\u26A0\uFE0F";
1487
- }
1488
- return "\u2139\uFE0F";
1489
- }
1490
- function calcDuration(start, stop) {
1491
- return Math.round((stop ?? performance.now()) - start);
1492
- }
1493
- function countCategoryAudits(refs, plugins) {
1494
- const groupLookup = plugins.reduce(
1495
- (lookup, plugin) => {
1496
- if (plugin.groups == null || plugin.groups.length === 0) {
1497
- return lookup;
1498
- }
1499
- return {
1500
- ...lookup,
1501
- [plugin.slug]: Object.fromEntries(
1502
- plugin.groups.map((group) => [group.slug, group])
1503
- )
1504
- };
1505
- },
1506
- {}
1507
- );
1508
- return refs.reduce((acc, ref) => {
1509
- if (ref.type === "group") {
1510
- const groupRefs = groupLookup[ref.plugin]?.[ref.slug]?.refs;
1511
- return acc + (groupRefs?.length ?? 0);
1512
- }
1513
- return acc + 1;
1514
- }, 0);
1515
- }
1516
- function getSortableAuditByRef({ slug, weight, plugin }, plugins) {
1517
- const auditPlugin = plugins.find((p) => p.slug === plugin);
1518
- if (!auditPlugin) {
1519
- throwIsNotPresentError(`Plugin ${plugin}`, "report");
1520
- }
1521
- const audit = auditPlugin.audits.find(
1522
- ({ slug: auditSlug }) => auditSlug === slug
1523
- );
1524
- if (!audit) {
1525
- throwIsNotPresentError(`Audit ${slug}`, auditPlugin.slug);
1526
- }
1527
- return {
1528
- ...audit,
1529
- weight,
1530
- plugin
1531
- };
1532
- }
1533
- function getSortableGroupByRef({ plugin, slug, weight }, plugins) {
1534
- const groupPlugin = plugins.find((p) => p.slug === plugin);
1535
- if (!groupPlugin) {
1536
- throwIsNotPresentError(`Plugin ${plugin}`, "report");
1537
- }
1538
- const group = groupPlugin.groups?.find(
1539
- ({ slug: groupSlug }) => groupSlug === slug
1540
- );
1541
- if (!group) {
1542
- throwIsNotPresentError(`Group ${slug}`, groupPlugin.slug);
1543
- }
1544
- const sortedAudits = getSortedGroupAudits(group, groupPlugin.slug, plugins);
1545
- const sortedAuditRefs = [...group.refs].sort((a, b) => {
1546
- const aIndex = sortedAudits.findIndex((ref) => ref.slug === a.slug);
1547
- const bIndex = sortedAudits.findIndex((ref) => ref.slug === b.slug);
1548
- return aIndex - bIndex;
1549
- });
1550
- return {
1551
- ...group,
1552
- refs: sortedAuditRefs,
1553
- plugin,
1554
- weight
1555
- };
1556
- }
1557
- function getSortedGroupAudits(group, plugin, plugins) {
1558
- return group.refs.map(
1559
- (ref) => getSortableAuditByRef(
1560
- {
1561
- plugin,
1562
- slug: ref.slug,
1563
- weight: ref.weight,
1564
- type: "audit"
1565
- },
1566
- plugins
1567
- )
1568
- ).sort(compareCategoryAuditsAndGroups);
1569
- }
1570
- function compareCategoryAuditsAndGroups(a, b) {
1571
- if (a.weight !== b.weight) {
1572
- return b.weight - a.weight;
1573
- }
1574
- if (a.score !== b.score) {
1575
- return a.score - b.score;
1576
- }
1577
- if ("value" in a && "value" in b && a.value !== b.value) {
1578
- return b.value - a.value;
1579
- }
1580
- return a.title.localeCompare(b.title);
1581
- }
1582
- function compareAudits(a, b) {
1583
- if (a.score !== b.score) {
1584
- return a.score - b.score;
1585
- }
1586
- if (a.value !== b.value) {
1587
- return b.value - a.value;
1588
- }
1589
- return a.title.localeCompare(b.title);
1590
- }
1591
- function compareIssueSeverity(severity1, severity2) {
1592
- const levels = {
1593
- info: 0,
1594
- warning: 1,
1595
- error: 2
1596
- };
1597
- return levels[severity1] - levels[severity2];
1598
- }
1599
- async function loadReport(options) {
1600
- const { outputDir, filename, format } = options;
1601
- await ensureDirectoryExists(outputDir);
1602
- const filePath = join2(outputDir, `${filename}.${format}`);
1603
- if (format === "json") {
1604
- const content = await readJsonFile(filePath);
1605
- return reportSchema.parse(content);
1606
- }
1607
- const text = await readTextFile(filePath);
1608
- return text;
1609
- }
1610
- function throwIsNotPresentError(itemName, presentPlace) {
1611
- throw new Error(`${itemName} is not present in ${presentPlace}`);
1612
- }
1613
- function getPluginNameFromSlug(slug, plugins) {
1614
- return plugins.find(({ slug: pluginSlug }) => pluginSlug === slug)?.title || slug;
1615
- }
1616
- function compareIssues(a, b) {
1617
- if (a.severity !== b.severity) {
1618
- return -compareIssueSeverity(a.severity, b.severity);
1619
- }
1620
- if (!a.source && b.source) {
1621
- return -1;
1622
- }
1623
- if (a.source && !b.source) {
1624
- return 1;
1625
- }
1626
- if (a.source?.file !== b.source?.file) {
1627
- return a.source?.file.localeCompare(b.source?.file || "") ?? 0;
1628
- }
1629
- if (!a.source?.position && b.source?.position) {
1630
- return -1;
1631
- }
1632
- if (a.source?.position && !b.source?.position) {
1633
- return 1;
1634
- }
1635
- if (a.source?.position?.startLine !== b.source?.position?.startLine) {
1636
- return (a.source?.position?.startLine ?? 0) - (b.source?.position?.startLine ?? 0);
1637
- }
1638
- return 0;
1639
- }
1640
-
1641
- // packages/utils/src/lib/execute-process.ts
1642
- var ProcessError = class extends Error {
1643
- code;
1644
- stderr;
1645
- stdout;
1646
- constructor(result) {
1647
- super(result.stderr);
1648
- this.code = result.code;
1649
- this.stderr = result.stderr;
1650
- this.stdout = result.stdout;
1651
- }
1652
- };
1653
- function executeProcess(cfg) {
1654
- const { observer, cwd, command, args, ignoreExitCode = false } = cfg;
1655
- const { onStdout, onError, onComplete } = observer ?? {};
1656
- const date = (/* @__PURE__ */ new Date()).toISOString();
1657
- const start = performance.now();
1658
- return new Promise((resolve, reject) => {
1659
- const process2 = spawn(command, args, { cwd, shell: true });
1660
- let stdout = "";
1661
- let stderr = "";
1662
- process2.stdout.on("data", (data) => {
1663
- stdout += String(data);
1664
- onStdout?.(String(data));
1665
- });
1666
- process2.stderr.on("data", (data) => {
1667
- stderr += String(data);
1668
- });
1669
- process2.on("error", (err) => {
1670
- stderr += err.toString();
1671
- });
1672
- process2.on("close", (code3) => {
1673
- const timings = { date, duration: calcDuration(start) };
1674
- if (code3 === 0 || ignoreExitCode) {
1675
- onComplete?.();
1676
- resolve({ code: code3, stdout, stderr, ...timings });
1677
- } else {
1678
- const errorMsg = new ProcessError({ code: code3, stdout, stderr, ...timings });
1679
- onError?.(errorMsg);
1680
- reject(errorMsg);
1681
- }
1682
- });
1683
- });
1684
- }
1685
-
1686
- // packages/utils/src/lib/filter.ts
1687
- function filterItemRefsBy(items, refFilterFn) {
1688
- return items.map((item) => ({
1689
- ...item,
1690
- refs: item.refs.filter(refFilterFn)
1691
- })).filter((item) => item.refs.length);
1692
- }
1693
-
1694
- // packages/utils/src/lib/git/git.ts
1695
- import { isAbsolute, join as join3, relative } from "node:path";
1696
- import { simpleGit } from "simple-git";
1697
- function getGitRoot(git = simpleGit()) {
1698
- return git.revparse("--show-toplevel");
1699
- }
1700
- function formatGitPath(path, gitRoot) {
1701
- const absolutePath = isAbsolute(path) ? path : join3(process.cwd(), path);
1702
- const relativePath = relative(gitRoot, absolutePath);
1703
- return toUnixPath(relativePath);
1704
- }
1705
- async function toGitPath(path, git = simpleGit()) {
1706
- const gitRoot = await getGitRoot(git);
1707
- return formatGitPath(path, gitRoot);
1708
- }
1709
- var GitStatusError = class _GitStatusError extends Error {
1710
- static ignoredProps = /* @__PURE__ */ new Set(["current", "tracking"]);
1711
- static getReducedStatus(status) {
1712
- return Object.fromEntries(
1713
- Object.entries(status).filter(([key]) => !this.ignoredProps.has(key)).filter(
1714
- (entry) => {
1715
- const value = entry[1];
1716
- if (value == null) {
1717
- return false;
1718
- }
1719
- if (Array.isArray(value) && value.length === 0) {
1720
- return false;
1721
- }
1722
- if (typeof value === "number" && value === 0) {
1723
- return false;
1724
- }
1725
- return !(typeof value === "boolean" && !value);
1726
- }
1727
- )
1728
- );
1729
- }
1730
- constructor(status) {
1731
- super(
1732
- `Working directory needs to be clean before we you can proceed. Commit your local changes or stash them:
1733
- ${JSON.stringify(
1734
- _GitStatusError.getReducedStatus(status),
1735
- null,
1736
- 2
1737
- )}`
1738
- );
1739
- }
1740
- };
1741
- async function guardAgainstLocalChanges(git = simpleGit()) {
1742
- const status = await git.status(["-s"]);
1743
- if (status.files.length > 0) {
1744
- throw new GitStatusError(status);
1745
- }
1746
- }
1747
- async function safeCheckout(branchOrHash, forceCleanStatus = false, git = simpleGit()) {
1748
- if (forceCleanStatus) {
1749
- await git.raw(["reset", "--hard"]);
1750
- await git.clean(["f", "d"]);
1751
- ui().logger.info(`git status cleaned`);
1752
- }
1753
- await guardAgainstLocalChanges(git);
1754
- await git.checkout(branchOrHash);
1755
- }
1756
-
1757
- // packages/utils/src/lib/git/git.commits-and-tags.ts
1758
- import { simpleGit as simpleGit2 } from "simple-git";
1759
-
1760
- // packages/utils/src/lib/semver.ts
1761
- import { rcompare, valid } from "semver";
1762
- function normalizeSemver(semverString) {
1763
- if (semverString.startsWith("v") || semverString.startsWith("V")) {
1764
- return semverString.slice(1);
1765
- }
1766
- if (semverString.includes("@")) {
1767
- return semverString.split("@").at(-1) ?? "";
1768
- }
1769
- return semverString;
1770
- }
1771
- function isSemver(semverString = "") {
1772
- return valid(normalizeSemver(semverString)) != null;
1773
- }
1774
- function sortSemvers(semverStrings) {
1775
- return semverStrings.map(normalizeSemver).filter(isSemver).sort(rcompare);
1776
- }
1777
-
1778
- // packages/utils/src/lib/git/git.commits-and-tags.ts
1779
- async function getLatestCommit(git = simpleGit2()) {
1780
- const log2 = await git.log({
1781
- maxCount: 1,
1782
- // git log -1 --pretty=format:"%H %s %an %aI" - See: https://git-scm.com/docs/pretty-formats
1783
- format: { hash: "%H", message: "%s", author: "%an", date: "%aI" }
1784
- });
1785
- return commitSchema.parse(log2.latest);
1786
- }
1787
- async function getCurrentBranchOrTag(git = simpleGit2()) {
1788
- return await git.branch().then((r) => r.current) || // If no current branch, try to get the tag
1789
- // @TODO use simple git
1790
- await git.raw(["describe", "--tags", "--exact-match"]).then((out) => out.trim());
1791
- }
1792
- function validateFilter({ from, to }) {
1793
- if (to && !from) {
1794
- throw new Error(
1795
- `filter needs the "from" option defined to accept the "to" option.
1796
- `
1797
- );
1798
- }
1799
- }
1800
- function filterLogs(allTags, opt) {
1801
- if (!opt) {
1802
- return allTags;
1803
- }
1804
- validateFilter(opt);
1805
- const { from, to, maxCount } = opt;
1806
- const finIndex = (tagName, fallback) => {
1807
- const idx = allTags.indexOf(tagName ?? "");
1808
- if (idx > -1) {
1809
- return idx;
1810
- }
1811
- return fallback;
1812
- };
1813
- const fromIndex = finIndex(from, 0);
1814
- const toIndex = finIndex(to, void 0);
1815
- return allTags.slice(fromIndex, toIndex ? toIndex + 1 : toIndex).slice(0, maxCount ?? void 0);
1492
+ validateFilter(opt);
1493
+ const { from, to, maxCount } = opt;
1494
+ const finIndex = (tagName, fallback) => {
1495
+ const idx = allTags.indexOf(tagName ?? "");
1496
+ if (idx > -1) {
1497
+ return idx;
1498
+ }
1499
+ return fallback;
1500
+ };
1501
+ const fromIndex = finIndex(from, 0);
1502
+ const toIndex = finIndex(to, void 0);
1503
+ return allTags.slice(fromIndex, toIndex ? toIndex + 1 : toIndex).slice(0, maxCount ?? void 0);
1816
1504
  }
1817
1505
  async function getHashFromTag(tag, git = simpleGit2()) {
1818
1506
  const tagDetails = await git.show(["--no-patch", "--format=%H", tag]);
@@ -1976,541 +1664,619 @@ function mergeUpload(a, b) {
1976
1664
  if (!a && !b) {
1977
1665
  return {};
1978
1666
  }
1979
- if (a) {
1980
- return b ? { upload: { ...a, ...b } } : {};
1667
+ if (a) {
1668
+ return b ? { upload: { ...a, ...b } } : {};
1669
+ } else {
1670
+ return { upload: b };
1671
+ }
1672
+ }
1673
+
1674
+ // packages/utils/src/lib/progress.ts
1675
+ import { black, bold as bold2, gray as gray2, green } from "ansis";
1676
+ import { MultiProgressBars } from "multi-progress-bars";
1677
+ var barStyles = {
1678
+ active: (s) => green(s),
1679
+ done: (s) => gray2(s),
1680
+ idle: (s) => gray2(s)
1681
+ };
1682
+ var messageStyles = {
1683
+ active: (s) => black(s),
1684
+ done: (s) => bold2.green(s),
1685
+ idle: (s) => gray2(s)
1686
+ };
1687
+ var mpb;
1688
+ function getSingletonProgressBars(options) {
1689
+ if (!mpb) {
1690
+ mpb = new MultiProgressBars({
1691
+ progressWidth: TERMINAL_WIDTH,
1692
+ initMessage: "",
1693
+ border: true,
1694
+ ...options
1695
+ });
1696
+ }
1697
+ return mpb;
1698
+ }
1699
+ function getProgressBar(taskName) {
1700
+ const tasks = getSingletonProgressBars();
1701
+ tasks.addTask(taskName, {
1702
+ type: "percentage",
1703
+ percentage: 0,
1704
+ message: "",
1705
+ barTransformFn: barStyles.idle
1706
+ });
1707
+ return {
1708
+ incrementInSteps: (numPlugins) => {
1709
+ tasks.incrementTask(taskName, {
1710
+ percentage: 1 / numPlugins,
1711
+ barTransformFn: barStyles.active
1712
+ });
1713
+ },
1714
+ updateTitle: (title) => {
1715
+ tasks.updateTask(taskName, {
1716
+ message: title,
1717
+ barTransformFn: barStyles.active
1718
+ });
1719
+ },
1720
+ endProgress: (message = "") => {
1721
+ tasks.done(taskName, {
1722
+ message: messageStyles.done(message),
1723
+ barTransformFn: barStyles.done
1724
+ });
1725
+ }
1726
+ };
1727
+ }
1728
+
1729
+ // packages/utils/src/lib/reports/flatten-plugins.ts
1730
+ function listGroupsFromAllPlugins(report) {
1731
+ return report.plugins.flatMap(
1732
+ (plugin) => plugin.groups?.map((group) => ({ plugin, group })) ?? []
1733
+ );
1734
+ }
1735
+ function listAuditsFromAllPlugins(report) {
1736
+ return report.plugins.flatMap(
1737
+ (plugin) => plugin.audits.map((audit) => ({ plugin, audit }))
1738
+ );
1739
+ }
1740
+
1741
+ // packages/utils/src/lib/reports/generate-md-report.ts
1742
+ import { MarkdownDocument as MarkdownDocument3, md as md4 } from "build-md";
1743
+
1744
+ // packages/utils/src/lib/text-formats/constants.ts
1745
+ var NEW_LINE = "\n";
1746
+ var TAB = " ";
1747
+ var SPACE = " ";
1748
+ var HIERARCHY = {
1749
+ level_1: 1,
1750
+ level_2: 2,
1751
+ level_3: 3,
1752
+ level_4: 4,
1753
+ level_5: 5,
1754
+ level_6: 6
1755
+ };
1756
+
1757
+ // packages/utils/src/lib/text-formats/html/details.ts
1758
+ function details(title, content, cfg = { open: false }) {
1759
+ return `<details${cfg.open ? " open" : ""}>${NEW_LINE}<summary>${title}</summary>${NEW_LINE}${// ⚠️ The blank line is needed to ensure Markdown in content is rendered correctly.
1760
+ NEW_LINE}${content}${NEW_LINE}${// @TODO in the future we could consider adding it only if the content ends with a code block
1761
+ // ⚠️ The blank line ensure Markdown in content is rendered correctly.
1762
+ NEW_LINE}</details>${// ⚠️ The blank line is needed to ensure Markdown after details is rendered correctly.
1763
+ NEW_LINE}`;
1764
+ }
1765
+
1766
+ // packages/utils/src/lib/text-formats/html/font-style.ts
1767
+ var boldElement = "b";
1768
+ function bold3(text) {
1769
+ return `<${boldElement}>${text}</${boldElement}>`;
1770
+ }
1771
+ var italicElement = "i";
1772
+ function italic(text) {
1773
+ return `<${italicElement}>${text}</${italicElement}>`;
1774
+ }
1775
+ var codeElement = "code";
1776
+ function code(text) {
1777
+ return `<${codeElement}>${text}</${codeElement}>`;
1778
+ }
1779
+
1780
+ // packages/utils/src/lib/text-formats/html/link.ts
1781
+ function link2(href, text) {
1782
+ return `<a href="${href}">${text || href}</a>`;
1783
+ }
1784
+
1785
+ // packages/utils/src/lib/text-formats/table.ts
1786
+ function rowToStringArray({ rows, columns = [] }) {
1787
+ if (Array.isArray(rows.at(0)) && typeof columns.at(0) === "object") {
1788
+ throw new TypeError(
1789
+ "Column can`t be object when rows are primitive values"
1790
+ );
1791
+ }
1792
+ return rows.map((row) => {
1793
+ if (Array.isArray(row)) {
1794
+ return row.map(String);
1795
+ }
1796
+ const objectRow = row;
1797
+ if (columns.length === 0 || typeof columns.at(0) === "string") {
1798
+ return Object.values(objectRow).map(
1799
+ (value) => value == null ? "" : String(value)
1800
+ );
1801
+ }
1802
+ return columns.map(
1803
+ ({ key }) => objectRow[key] == null ? "" : String(objectRow[key])
1804
+ );
1805
+ });
1806
+ }
1807
+ function columnsToStringArray({
1808
+ rows,
1809
+ columns = []
1810
+ }) {
1811
+ const firstRow = rows.at(0);
1812
+ const primitiveRows = Array.isArray(firstRow);
1813
+ if (typeof columns.at(0) === "string" && !primitiveRows) {
1814
+ throw new Error("invalid union type. Caught by model parsing.");
1815
+ }
1816
+ if (columns.length === 0) {
1817
+ if (Array.isArray(firstRow)) {
1818
+ return firstRow.map((_, idx) => String(idx));
1819
+ }
1820
+ return Object.keys(firstRow);
1821
+ }
1822
+ if (typeof columns.at(0) === "string") {
1823
+ return columns.map(String);
1824
+ }
1825
+ const cols = columns;
1826
+ return cols.map(({ label, key }) => label ?? capitalize(key));
1827
+ }
1828
+ function getColumnAlignmentForKeyAndIndex(targetKey, targetIdx, columns = []) {
1829
+ const column = columns.at(targetIdx) ?? columns.find((col) => col.key === targetKey);
1830
+ if (typeof column === "string") {
1831
+ return column;
1832
+ } else if (typeof column === "object") {
1833
+ return column.align ?? "center";
1981
1834
  } else {
1982
- return { upload: b };
1835
+ return "center";
1983
1836
  }
1984
1837
  }
1985
-
1986
- // packages/utils/src/lib/progress.ts
1987
- import chalk3 from "chalk";
1988
- import { MultiProgressBars } from "multi-progress-bars";
1989
- var barStyles = {
1990
- active: (s) => chalk3.green(s),
1991
- done: (s) => chalk3.gray(s),
1992
- idle: (s) => chalk3.gray(s)
1993
- };
1994
- var messageStyles = {
1995
- active: (s) => chalk3.black(s),
1996
- done: (s) => chalk3.green(chalk3.bold(s)),
1997
- idle: (s) => chalk3.gray(s)
1998
- };
1999
- var mpb;
2000
- function getSingletonProgressBars(options) {
2001
- if (!mpb) {
2002
- mpb = new MultiProgressBars({
2003
- progressWidth: TERMINAL_WIDTH,
2004
- initMessage: "",
2005
- border: true,
2006
- ...options
2007
- });
1838
+ function getColumnAlignmentForIndex(targetIdx, columns = []) {
1839
+ const column = columns.at(targetIdx);
1840
+ if (column == null) {
1841
+ return "center";
1842
+ } else if (typeof column === "string") {
1843
+ return column;
1844
+ } else if (typeof column === "object") {
1845
+ return column.align ?? "center";
1846
+ } else {
1847
+ return "center";
2008
1848
  }
2009
- return mpb;
2010
1849
  }
2011
- function getProgressBar(taskName) {
2012
- const tasks = getSingletonProgressBars();
2013
- tasks.addTask(taskName, {
2014
- type: "percentage",
2015
- percentage: 0,
2016
- message: "",
2017
- barTransformFn: barStyles.idle
2018
- });
2019
- return {
2020
- incrementInSteps: (numPlugins) => {
2021
- tasks.incrementTask(taskName, {
2022
- percentage: 1 / numPlugins,
2023
- barTransformFn: barStyles.active
2024
- });
2025
- },
2026
- updateTitle: (title) => {
2027
- tasks.updateTask(taskName, {
2028
- message: title,
2029
- barTransformFn: barStyles.active
2030
- });
2031
- },
2032
- endProgress: (message = "") => {
2033
- tasks.done(taskName, {
2034
- message: messageStyles.done(message),
2035
- barTransformFn: barStyles.done
2036
- });
2037
- }
2038
- };
1850
+ function getColumnAlignments(tableData) {
1851
+ const { rows, columns = [] } = tableData;
1852
+ if (rows.at(0) == null) {
1853
+ throw new Error("first row can`t be undefined.");
1854
+ }
1855
+ if (Array.isArray(rows.at(0))) {
1856
+ const firstPrimitiveRow = rows.at(0);
1857
+ return Array.from({ length: firstPrimitiveRow.length }).map(
1858
+ (_, idx) => getColumnAlignmentForIndex(idx, columns)
1859
+ );
1860
+ }
1861
+ const biggestRow = [...rows].sort((a, b) => Object.keys(a).length - Object.keys(b).length).at(-1);
1862
+ if (columns.length > 0) {
1863
+ return columns.map(
1864
+ (column, idx) => typeof column === "string" ? column : getColumnAlignmentForKeyAndIndex(
1865
+ column.key,
1866
+ idx,
1867
+ columns
1868
+ )
1869
+ );
1870
+ }
1871
+ return Object.keys(biggestRow ?? {}).map((_) => "center");
2039
1872
  }
2040
1873
 
2041
- // packages/utils/src/lib/reports/flatten-plugins.ts
2042
- function listGroupsFromAllPlugins(report) {
2043
- return report.plugins.flatMap(
2044
- (plugin) => plugin.groups?.map((group) => ({ plugin, group })) ?? []
2045
- );
1874
+ // packages/utils/src/lib/text-formats/html/table.ts
1875
+ function wrap(elem, content) {
1876
+ return `<${elem}>${content}</${elem}>${NEW_LINE}`;
2046
1877
  }
2047
- function listAuditsFromAllPlugins(report) {
2048
- return report.plugins.flatMap(
2049
- (plugin) => plugin.audits.map((audit) => ({ plugin, audit }))
2050
- );
1878
+ function wrapRow(content) {
1879
+ const elem = "tr";
1880
+ return `<${elem}>${NEW_LINE}${content}</${elem}>${NEW_LINE}`;
1881
+ }
1882
+ function table(tableData) {
1883
+ if (tableData.rows.length === 0) {
1884
+ throw new Error("Data can't be empty");
1885
+ }
1886
+ const tableHeaderCols = columnsToStringArray(tableData).map((s) => wrap("th", s)).join("");
1887
+ const tableHeaderRow = wrapRow(tableHeaderCols);
1888
+ const tableBody = rowToStringArray(tableData).map((arr) => {
1889
+ const columns = arr.map((s) => wrap("td", s)).join("");
1890
+ return wrapRow(columns);
1891
+ }).join("");
1892
+ return wrap("table", `${NEW_LINE}${tableHeaderRow}${tableBody}`);
2051
1893
  }
2052
1894
 
1895
+ // packages/utils/src/lib/text-formats/index.ts
1896
+ var html = {
1897
+ bold: bold3,
1898
+ italic,
1899
+ code,
1900
+ link: link2,
1901
+ details,
1902
+ table
1903
+ };
1904
+
2053
1905
  // packages/utils/src/lib/reports/formatting.ts
2054
- var { headline: headline2, lines: lines2, link: link4, section: section2, table: table3 } = md;
1906
+ import {
1907
+ MarkdownDocument,
1908
+ md as md2
1909
+ } from "build-md";
2055
1910
  function tableSection(tableData, options) {
2056
1911
  if (tableData.rows.length === 0) {
2057
- return "";
2058
- }
2059
- const { level = 4 } = options ?? {};
2060
- const render = (h7, l) => l === 0 ? h7 : headline2(h7, l);
2061
- return lines2(
2062
- tableData.title && render(tableData.title, level),
2063
- table3(tableData)
1912
+ return null;
1913
+ }
1914
+ const { level = HIERARCHY.level_4 } = options ?? {};
1915
+ const columns = columnsToStringArray(tableData);
1916
+ const alignments = getColumnAlignments(tableData);
1917
+ const rows = rowToStringArray(tableData);
1918
+ return new MarkdownDocument().heading(level, tableData.title).table(
1919
+ columns.map((heading, i) => {
1920
+ const alignment = alignments[i];
1921
+ if (alignment) {
1922
+ return { heading, alignment };
1923
+ }
1924
+ return heading;
1925
+ }),
1926
+ rows
2064
1927
  );
2065
1928
  }
2066
- function metaDescription({
2067
- docsUrl,
2068
- description
2069
- }) {
1929
+ function metaDescription(audit) {
1930
+ const docsUrl = audit.docsUrl;
1931
+ const description = audit.description?.trim();
2070
1932
  if (docsUrl) {
2071
- const docsLink = link4(docsUrl, "\u{1F4D6} Docs");
1933
+ const docsLink = md2.link(docsUrl, "\u{1F4D6} Docs");
2072
1934
  if (!description) {
2073
- return section2(docsLink);
1935
+ return docsLink;
2074
1936
  }
2075
- const parsedDescription = description.toString().endsWith("```") ? `${description}${NEW_LINE + NEW_LINE}` : `${description}${SPACE}`;
2076
- return section2(`${parsedDescription}${docsLink}`);
1937
+ const parsedDescription = description.endsWith("```") ? `${description}
1938
+
1939
+ ` : `${description} `;
1940
+ return md2`${parsedDescription}${docsLink}`;
2077
1941
  }
2078
1942
  if (description && description.trim().length > 0) {
2079
- return section2(description);
1943
+ return description;
2080
1944
  }
2081
1945
  return "";
2082
1946
  }
2083
1947
 
2084
1948
  // packages/utils/src/lib/reports/generate-md-report-categoy-section.ts
2085
- var { link: link5, section: section3, h2: h22, lines: lines3, li: li2, bold: boldMd2, h3: h32, indentation: indentation2 } = md;
2086
- function categoriesOverviewSection(report) {
2087
- const { categories, plugins } = report;
2088
- if (categories.length > 0 && plugins.length > 0) {
2089
- const tableContent = {
2090
- columns: reportOverviewTableHeaders,
2091
- rows: categories.map(({ title, refs, score }) => ({
2092
- // The heading "ID" is inferred from the heading text in Markdown.
2093
- category: link5(`#${slugify(title)}`, title),
2094
- score: `${scoreMarker(score)}${SPACE}${boldMd2(
2095
- formatReportScore(score)
2096
- )}`,
2097
- audits: countCategoryAudits(refs, plugins).toString()
2098
- }))
2099
- };
2100
- return tableSection(tableContent);
1949
+ import { MarkdownDocument as MarkdownDocument2, md as md3 } from "build-md";
1950
+
1951
+ // packages/utils/src/lib/reports/sorting.ts
1952
+ function getSortableAuditByRef({ slug, weight, plugin }, plugins) {
1953
+ const auditPlugin = plugins.find((p) => p.slug === plugin);
1954
+ if (!auditPlugin) {
1955
+ throwIsNotPresentError(`Plugin ${plugin}`, "report");
2101
1956
  }
2102
- return "";
1957
+ const audit = auditPlugin.audits.find(
1958
+ ({ slug: auditSlug }) => auditSlug === slug
1959
+ );
1960
+ if (!audit) {
1961
+ throwIsNotPresentError(`Audit ${slug}`, auditPlugin.slug);
1962
+ }
1963
+ return {
1964
+ ...audit,
1965
+ weight,
1966
+ plugin
1967
+ };
2103
1968
  }
2104
- function categoriesDetailsSection(report) {
1969
+ function getSortedGroupAudits(group, plugin, plugins) {
1970
+ return group.refs.map(
1971
+ (ref) => getSortableAuditByRef(
1972
+ {
1973
+ plugin,
1974
+ slug: ref.slug,
1975
+ weight: ref.weight,
1976
+ type: "audit"
1977
+ },
1978
+ plugins
1979
+ )
1980
+ ).sort(compareCategoryAuditsAndGroups);
1981
+ }
1982
+ function getSortableGroupByRef({ plugin, slug, weight }, plugins) {
1983
+ const groupPlugin = plugins.find((p) => p.slug === plugin);
1984
+ if (!groupPlugin) {
1985
+ throwIsNotPresentError(`Plugin ${plugin}`, "report");
1986
+ }
1987
+ const group = groupPlugin.groups?.find(
1988
+ ({ slug: groupSlug }) => groupSlug === slug
1989
+ );
1990
+ if (!group) {
1991
+ throwIsNotPresentError(`Group ${slug}`, groupPlugin.slug);
1992
+ }
1993
+ const sortedAudits = getSortedGroupAudits(group, groupPlugin.slug, plugins);
1994
+ const sortedAuditRefs = [...group.refs].sort((a, b) => {
1995
+ const aIndex = sortedAudits.findIndex((ref) => ref.slug === a.slug);
1996
+ const bIndex = sortedAudits.findIndex((ref) => ref.slug === b.slug);
1997
+ return aIndex - bIndex;
1998
+ });
1999
+ return {
2000
+ ...group,
2001
+ refs: sortedAuditRefs,
2002
+ plugin,
2003
+ weight
2004
+ };
2005
+ }
2006
+ function sortReport(report) {
2105
2007
  const { categories, plugins } = report;
2106
- const categoryDetails = categories.flatMap((category) => {
2107
- const categoryTitle = h32(category.title);
2108
- const categoryScore = `${scoreMarker(
2109
- category.score
2110
- )}${SPACE}Score: ${boldMd2(formatReportScore(category.score))}`;
2111
- const categoryMDItems = category.refs.map((ref) => {
2112
- if (ref.type === "group") {
2113
- const group = getSortableGroupByRef(ref, plugins);
2114
- const groupAudits = group.refs.map(
2115
- (groupRef) => getSortableAuditByRef(
2116
- { ...groupRef, plugin: group.plugin, type: "audit" },
2117
- plugins
2118
- )
2119
- );
2120
- const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
2121
- return categoryGroupItem(group, groupAudits, pluginTitle);
2122
- } else {
2123
- const audit = getSortableAuditByRef(ref, plugins);
2124
- const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
2125
- return categoryRef(audit, pluginTitle);
2126
- }
2127
- });
2128
- return section3(
2129
- categoryTitle,
2130
- metaDescription(category),
2131
- categoryScore,
2132
- ...categoryMDItems
2008
+ const sortedCategories = categories.map((category) => {
2009
+ const { audits, groups } = category.refs.reduce(
2010
+ (acc, ref) => ({
2011
+ ...acc,
2012
+ ...ref.type === "group" ? {
2013
+ groups: [...acc.groups, getSortableGroupByRef(ref, plugins)]
2014
+ } : {
2015
+ audits: [...acc.audits, getSortableAuditByRef(ref, plugins)]
2016
+ }
2017
+ }),
2018
+ { groups: [], audits: [] }
2133
2019
  );
2020
+ const sortedAuditsAndGroups = [...audits, ...groups].sort(
2021
+ compareCategoryAuditsAndGroups
2022
+ );
2023
+ const sortedRefs = [...category.refs].sort((a, b) => {
2024
+ const aIndex = sortedAuditsAndGroups.findIndex(
2025
+ (ref) => ref.slug === a.slug && ref.plugin === a.plugin
2026
+ );
2027
+ const bIndex = sortedAuditsAndGroups.findIndex(
2028
+ (ref) => ref.slug === b.slug && ref.plugin === b.plugin
2029
+ );
2030
+ return aIndex - bIndex;
2031
+ });
2032
+ return { ...category, refs: sortedRefs };
2134
2033
  });
2135
- return lines3(h22(CATEGORIES_TITLE), ...categoryDetails);
2034
+ return {
2035
+ ...report,
2036
+ categories: sortedCategories,
2037
+ plugins: sortPlugins(plugins)
2038
+ };
2039
+ }
2040
+ function sortPlugins(plugins) {
2041
+ return plugins.map((plugin) => ({
2042
+ ...plugin,
2043
+ audits: [...plugin.audits].sort(compareAudits).map(
2044
+ (audit) => audit.details?.issues ? {
2045
+ ...audit,
2046
+ details: {
2047
+ ...audit.details,
2048
+ issues: [...audit.details.issues].sort(compareIssues)
2049
+ }
2050
+ } : audit
2051
+ )
2052
+ }));
2053
+ }
2054
+
2055
+ // packages/utils/src/lib/reports/generate-md-report-categoy-section.ts
2056
+ function categoriesOverviewSection(report) {
2057
+ const { categories, plugins } = report;
2058
+ return new MarkdownDocument2().table(
2059
+ [
2060
+ { heading: "\u{1F3F7} Category", alignment: "left" },
2061
+ { heading: "\u2B50 Score", alignment: "center" },
2062
+ { heading: "\u{1F6E1} Audits", alignment: "center" }
2063
+ ],
2064
+ categories.map(({ title, refs, score, isBinary }) => [
2065
+ // @TODO refactor `isBinary: boolean` to `targetScore: number` #713
2066
+ // The heading "ID" is inferred from the heading text in Markdown.
2067
+ md3.link(`#${slugify(title)}`, title),
2068
+ md3`${scoreMarker(score)} ${md3.bold(
2069
+ formatReportScore(score)
2070
+ )}${binaryIconSuffix(score, isBinary)}`,
2071
+ countCategoryAudits(refs, plugins).toString()
2072
+ ])
2073
+ );
2074
+ }
2075
+ function categoriesDetailsSection(report) {
2076
+ const { categories, plugins } = report;
2077
+ return new MarkdownDocument2().heading(HIERARCHY.level_2, "\u{1F3F7} Categories").$foreach(
2078
+ categories,
2079
+ (doc, category) => doc.heading(HIERARCHY.level_3, category.title).paragraph(metaDescription(category)).paragraph(
2080
+ md3`${scoreMarker(category.score)} Score: ${md3.bold(
2081
+ formatReportScore(category.score)
2082
+ )}${binaryIconSuffix(category.score, category.isBinary)}`
2083
+ ).list(
2084
+ category.refs.map((ref) => {
2085
+ if (ref.type === "group") {
2086
+ const group = getSortableGroupByRef(ref, plugins);
2087
+ const groupAudits = group.refs.map(
2088
+ (groupRef) => getSortableAuditByRef(
2089
+ { ...groupRef, plugin: group.plugin, type: "audit" },
2090
+ plugins
2091
+ )
2092
+ );
2093
+ const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
2094
+ return categoryGroupItem(group, groupAudits, pluginTitle);
2095
+ } else {
2096
+ const audit = getSortableAuditByRef(ref, plugins);
2097
+ const pluginTitle = getPluginNameFromSlug(ref.plugin, plugins);
2098
+ return categoryRef(audit, pluginTitle);
2099
+ }
2100
+ })
2101
+ )
2102
+ );
2136
2103
  }
2137
2104
  function categoryRef({ title, score, value, displayValue }, pluginTitle) {
2138
- const auditTitleAsLink = link5(
2105
+ const auditTitleAsLink = md3.link(
2139
2106
  `#${slugify(title)}-${slugify(pluginTitle)}`,
2140
2107
  title
2141
2108
  );
2142
2109
  const marker = scoreMarker(score, "square");
2143
- return li2(
2144
- `${marker}${SPACE}${auditTitleAsLink}${SPACE}(_${pluginTitle}_) - ${boldMd2(
2145
- (displayValue || value).toString()
2146
- )}`
2147
- );
2110
+ return md3`${marker} ${auditTitleAsLink} (${md3.italic(
2111
+ pluginTitle
2112
+ )}) - ${md3.bold((displayValue || value).toString())}`;
2148
2113
  }
2149
2114
  function categoryGroupItem({ score = 0, title }, groupAudits, pluginTitle) {
2150
- const groupTitle = li2(
2151
- `${scoreMarker(score)}${SPACE}${title}${SPACE}(_${pluginTitle}_)`
2152
- );
2153
- const auditTitles = groupAudits.map(
2154
- ({ title: auditTitle, score: auditScore, value, displayValue }) => {
2155
- const auditTitleLink = link5(
2156
- `#${slugify(auditTitle)}-${slugify(pluginTitle)}`,
2157
- auditTitle
2158
- );
2159
- const marker = scoreMarker(auditScore, "square");
2160
- return indentation2(
2161
- li2(
2162
- `${marker}${SPACE}${auditTitleLink} - ${boldMd2(
2163
- String(displayValue ?? value)
2164
- )}`
2165
- )
2166
- );
2167
- }
2115
+ const groupTitle = md3`${scoreMarker(score)} ${title} (${md3.italic(
2116
+ pluginTitle
2117
+ )})`;
2118
+ const auditsList = md3.list(
2119
+ groupAudits.map(
2120
+ ({ title: auditTitle, score: auditScore, value, displayValue }) => {
2121
+ const auditTitleLink = md3.link(
2122
+ `#${slugify(auditTitle)}-${slugify(pluginTitle)}`,
2123
+ auditTitle
2124
+ );
2125
+ const marker = scoreMarker(auditScore, "square");
2126
+ return md3`${marker} ${auditTitleLink} - ${md3.bold(
2127
+ String(displayValue ?? value)
2128
+ )}`;
2129
+ }
2130
+ )
2168
2131
  );
2169
- return lines3(groupTitle, ...auditTitles);
2132
+ return md3`${groupTitle}${auditsList}`;
2133
+ }
2134
+ function binaryIconSuffix(score, isBinary) {
2135
+ return targetScoreIcon(score, isBinary ? 1 : void 0, { prefix: " " });
2170
2136
  }
2171
2137
 
2172
2138
  // packages/utils/src/lib/reports/generate-md-report.ts
2173
- var { h1: h12, h2: h23, h3: h33, lines: lines4, link: link6, section: section4, code: codeMd } = md;
2174
- var { bold: boldHtml, details: details2 } = html;
2175
2139
  function auditDetailsAuditValue({
2176
2140
  score,
2177
2141
  value,
2178
2142
  displayValue
2179
2143
  }) {
2180
- return `${scoreMarker(score, "square")} ${boldHtml(
2144
+ return md4`${scoreMarker(score, "square")} ${md4.bold(
2181
2145
  String(displayValue ?? value)
2182
2146
  )} (score: ${formatReportScore(score)})`;
2183
2147
  }
2184
2148
  function generateMdReport(report) {
2185
- const printCategories = report.categories.length > 0;
2186
- return lines4(
2187
- h12(reportHeadlineText),
2188
- printCategories ? categoriesOverviewSection(report) : "",
2189
- printCategories ? categoriesDetailsSection(report) : "",
2190
- auditsSection(report),
2191
- aboutSection(report),
2192
- `${FOOTER_PREFIX}${SPACE}${link6(README_LINK, "Code PushUp")}`
2193
- );
2149
+ return new MarkdownDocument3().heading(HIERARCHY.level_1, REPORT_HEADLINE_TEXT).$if(
2150
+ report.categories.length > 0,
2151
+ (doc) => doc.$concat(
2152
+ categoriesOverviewSection(report),
2153
+ categoriesDetailsSection(report)
2154
+ )
2155
+ ).$concat(auditsSection(report), aboutSection(report)).rule().paragraph(md4`${FOOTER_PREFIX} ${md4.link(README_LINK, "Code PushUp")}`).toString();
2194
2156
  }
2195
2157
  function auditDetailsIssues(issues = []) {
2196
2158
  if (issues.length === 0) {
2197
- return "";
2198
- }
2199
- const detailsTableData = {
2200
- title: "Issues",
2201
- columns: issuesTableHeadings,
2202
- rows: issues.map(
2203
- ({ severity: severityVal, message, source: sourceVal }) => {
2204
- const severity = `${severityMarker(severityVal)} <i>${severityVal}</i>`;
2205
- if (!sourceVal) {
2206
- return { severity, message, file: "", line: "" };
2207
- }
2208
- const file = `<code>${sourceVal.file}</code>`;
2209
- if (!sourceVal.position) {
2210
- return { severity, message, file, line: "" };
2211
- }
2212
- const { startLine, endLine } = sourceVal.position;
2213
- const line = `${startLine || ""}${endLine && startLine !== endLine ? `-${endLine}` : ""}`;
2214
- return { severity, message, file, line };
2159
+ return null;
2160
+ }
2161
+ return new MarkdownDocument3().heading(HIERARCHY.level_4, "Issues").table(
2162
+ [
2163
+ { heading: "Severity", alignment: "center" },
2164
+ { heading: "Message", alignment: "left" },
2165
+ { heading: "Source file", alignment: "left" },
2166
+ { heading: "Line(s)", alignment: "center" }
2167
+ ],
2168
+ issues.map(({ severity: level, message, source }) => {
2169
+ const severity = md4`${severityMarker(level)} ${md4.italic(level)}`;
2170
+ if (!source) {
2171
+ return [severity, message];
2215
2172
  }
2216
- )
2217
- };
2218
- return tableSection(detailsTableData);
2173
+ const file = md4.code(source.file);
2174
+ if (!source.position) {
2175
+ return [severity, message, file];
2176
+ }
2177
+ const { startLine, endLine } = source.position;
2178
+ const line = `${startLine || ""}${endLine && startLine !== endLine ? `-${endLine}` : ""}`;
2179
+ return [severity, message, file, line];
2180
+ })
2181
+ );
2219
2182
  }
2220
2183
  function auditDetails(audit) {
2221
- const { table: table5, issues = [] } = audit.details ?? {};
2184
+ const { table: table2, issues = [] } = audit.details ?? {};
2222
2185
  const detailsValue = auditDetailsAuditValue(audit);
2223
- if (issues.length === 0 && table5 == null) {
2224
- return section4(detailsValue);
2186
+ if (issues.length === 0 && !table2?.rows.length) {
2187
+ return new MarkdownDocument3().paragraph(detailsValue);
2225
2188
  }
2226
- const tableSectionContent = table5 == null ? "" : tableSection(table5);
2227
- const issuesSectionContent = issues.length > 0 ? auditDetailsIssues(issues) : "";
2228
- return details2(
2189
+ const tableSectionContent = table2 && tableSection(table2);
2190
+ const issuesSectionContent = issues.length > 0 && auditDetailsIssues(issues);
2191
+ return new MarkdownDocument3().details(
2229
2192
  detailsValue,
2230
- lines4(tableSectionContent, issuesSectionContent)
2193
+ new MarkdownDocument3().$concat(tableSectionContent, issuesSectionContent)
2231
2194
  );
2232
2195
  }
2233
2196
  function auditsSection({
2234
2197
  plugins
2235
2198
  }) {
2236
- const content = plugins.flatMap(
2237
- ({ slug, audits }) => audits.flatMap((audit) => {
2238
- const auditTitle = `${audit.title}${SPACE}(${getPluginNameFromSlug(
2239
- slug,
2240
- plugins
2241
- )})`;
2199
+ return new MarkdownDocument3().heading(HIERARCHY.level_2, "\u{1F6E1}\uFE0F Audits").$foreach(
2200
+ plugins.flatMap(
2201
+ (plugin) => plugin.audits.map((audit) => ({ ...audit, plugin }))
2202
+ ),
2203
+ (doc, { plugin, ...audit }) => {
2204
+ const auditTitle = `${audit.title} (${plugin.title})`;
2242
2205
  const detailsContent = auditDetails(audit);
2243
2206
  const descriptionContent = metaDescription(audit);
2244
- return [h33(auditTitle), detailsContent, descriptionContent];
2245
- })
2207
+ return doc.heading(HIERARCHY.level_3, auditTitle).$concat(detailsContent).paragraph(descriptionContent);
2208
+ }
2246
2209
  );
2247
- return section4(h23("\u{1F6E1}\uFE0F Audits"), ...content);
2248
2210
  }
2249
2211
  function aboutSection(report) {
2250
2212
  const { date, plugins } = report;
2251
- const reportMetaTable = reportMetaData(report);
2252
- const pluginMetaTable = reportPluginMeta({ plugins });
2253
- return lines4(
2254
- h23("About"),
2255
- section4(
2256
- `Report was created by [Code PushUp](${README_LINK}) on ${formatDate(
2257
- new Date(date)
2258
- )}.`
2259
- ),
2260
- tableSection(pluginMetaTable),
2261
- tableSection(reportMetaTable)
2262
- );
2263
- }
2264
- function reportPluginMeta({ plugins }) {
2265
- return {
2266
- columns: [
2267
- {
2268
- key: "plugin",
2269
- align: "left"
2270
- },
2271
- {
2272
- key: "audits"
2273
- },
2274
- {
2275
- key: "version"
2276
- },
2277
- {
2278
- key: "duration"
2279
- }
2213
+ return new MarkdownDocument3().heading(HIERARCHY.level_2, "About").paragraph(
2214
+ md4`Report was created by ${md4.link(
2215
+ README_LINK,
2216
+ "Code PushUp"
2217
+ )} on ${formatDate(new Date(date))}.`
2218
+ ).table(...pluginMetaTable({ plugins })).table(...reportMetaTable(report));
2219
+ }
2220
+ function pluginMetaTable({
2221
+ plugins
2222
+ }) {
2223
+ return [
2224
+ [
2225
+ { heading: "Plugin", alignment: "left" },
2226
+ { heading: "Audits", alignment: "center" },
2227
+ { heading: "Version", alignment: "center" },
2228
+ { heading: "Duration", alignment: "right" }
2280
2229
  ],
2281
- rows: plugins.map(
2282
- ({
2283
- title: pluginTitle,
2284
- audits,
2285
- version: pluginVersion,
2286
- duration: pluginDuration
2287
- }) => ({
2288
- plugin: pluginTitle,
2289
- audits: audits.length.toString(),
2290
- version: codeMd(pluginVersion || ""),
2291
- duration: formatDuration(pluginDuration)
2292
- })
2293
- )
2294
- };
2230
+ plugins.map(({ title, audits, version = "", duration }) => [
2231
+ title,
2232
+ audits.length.toString(),
2233
+ version && md4.code(version),
2234
+ formatDuration(duration)
2235
+ ])
2236
+ ];
2295
2237
  }
2296
- function reportMetaData({
2238
+ function reportMetaTable({
2297
2239
  commit,
2298
2240
  version,
2299
2241
  duration,
2300
2242
  plugins,
2301
2243
  categories
2302
2244
  }) {
2303
- const commitInfo = commit ? `${commit.message}${SPACE}(${commit.hash})` : "N/A";
2304
- return {
2305
- columns: [
2306
- {
2307
- key: "commit",
2308
- align: "left"
2309
- },
2310
- {
2311
- key: "version"
2312
- },
2313
- {
2314
- key: "duration"
2315
- },
2316
- {
2317
- key: "plugins"
2318
- },
2319
- {
2320
- key: "categories"
2321
- },
2322
- {
2323
- key: "audits"
2324
- }
2245
+ return [
2246
+ [
2247
+ { heading: "Commit", alignment: "left" },
2248
+ { heading: "Version", alignment: "center" },
2249
+ { heading: "Duration", alignment: "right" },
2250
+ { heading: "Plugins", alignment: "center" },
2251
+ { heading: "Categories", alignment: "center" },
2252
+ { heading: "Audits", alignment: "center" }
2325
2253
  ],
2326
- rows: [
2327
- {
2328
- commit: commitInfo,
2329
- version: codeMd(version || ""),
2330
- duration: formatDuration(duration),
2331
- plugins: plugins.length,
2332
- categories: categories.length,
2333
- audits: plugins.reduce((acc, { audits }) => acc + audits.length, 0).toString()
2334
- }
2254
+ [
2255
+ [
2256
+ commit ? `${commit.message} (${commit.hash})` : "N/A",
2257
+ md4.code(version),
2258
+ formatDuration(duration),
2259
+ plugins.length.toString(),
2260
+ categories.length.toString(),
2261
+ plugins.reduce((acc, { audits }) => acc + audits.length, 0).toString()
2262
+ ]
2335
2263
  ]
2336
- };
2264
+ ];
2337
2265
  }
2338
2266
 
2339
2267
  // packages/utils/src/lib/reports/generate-md-reports-diff.ts
2340
- var {
2341
- h1: h13,
2342
- h2: h24,
2343
- lines: lines5,
2344
- link: link7,
2345
- bold: boldMd3,
2346
- italic: italicMd,
2347
- table: table4,
2348
- section: section5
2349
- } = md;
2350
- var { details: details3 } = html;
2268
+ import {
2269
+ MarkdownDocument as MarkdownDocument5,
2270
+ md as md6
2271
+ } from "build-md";
2272
+
2273
+ // packages/utils/src/lib/reports/generate-md-reports-diff-utils.ts
2274
+ import { MarkdownDocument as MarkdownDocument4, md as md5 } from "build-md";
2351
2275
  var MAX_ROWS = 100;
2352
- function generateMdReportsDiff(diff) {
2353
- return lines5(
2354
- section5(formatDiffHeaderSection(diff)),
2355
- formatDiffCategoriesSection(diff),
2356
- formatDiffGroupsSection(diff),
2357
- formatDiffAuditsSection(diff)
2358
- );
2359
- }
2360
- function formatDiffHeaderSection(diff) {
2361
- const outcomeTexts = {
2362
- positive: `\u{1F973} Code PushUp report has ${boldMd3("improved")}`,
2363
- negative: `\u{1F61F} Code PushUp report has ${boldMd3("regressed")}`,
2364
- mixed: `\u{1F928} Code PushUp report has both ${boldMd3(
2365
- "improvements and regressions"
2366
- )}`,
2367
- unchanged: `\u{1F610} Code PushUp report is ${boldMd3("unchanged")}`
2368
- };
2369
- const outcome = mergeDiffOutcomes(
2370
- changesToDiffOutcomes([
2371
- ...diff.categories.changed,
2372
- ...diff.groups.changed,
2373
- ...diff.audits.changed
2374
- ])
2375
- );
2376
- const styleCommits = (commits) => `compared target commit ${commits.after.hash} with source commit ${commits.before.hash}`;
2377
- return lines5(
2378
- h13("Code PushUp"),
2379
- diff.commits ? `${outcomeTexts[outcome]} \u2013 ${styleCommits(diff.commits)}.` : `${outcomeTexts[outcome]}.`
2380
- );
2381
- }
2382
- function formatDiffCategoriesSection(diff) {
2383
- const { changed, unchanged, added } = diff.categories;
2384
- const categoriesCount = changed.length + unchanged.length + added.length;
2385
- const hasChanges = unchanged.length < categoriesCount;
2386
- if (categoriesCount === 0) {
2387
- return "";
2388
- }
2389
- const columns = [
2390
- { key: "category", label: "\u{1F3F7}\uFE0F Category", align: "left" },
2391
- { key: "before", label: hasChanges ? "\u2B50 Previous score" : "\u2B50 Score" },
2392
- { key: "after", label: "\u2B50 Current score" },
2393
- { key: "change", label: "\u{1F504} Score change" }
2394
- ];
2395
- return lines5(
2396
- h24("\u{1F3F7}\uFE0F Categories"),
2397
- categoriesCount > 0 && table4({
2398
- columns: hasChanges ? columns : columns.slice(0, 2),
2399
- rows: [
2400
- ...sortChanges(changed).map((category) => ({
2401
- category: formatTitle(category),
2402
- after: formatScoreWithColor(category.scores.after),
2403
- before: formatScoreWithColor(category.scores.before, {
2404
- skipBold: true
2405
- }),
2406
- change: formatScoreChange(category.scores.diff)
2407
- })),
2408
- ...added.map((category) => ({
2409
- category: formatTitle(category),
2410
- after: formatScoreWithColor(category.score),
2411
- before: italicMd("n/a (\\*)"),
2412
- change: italicMd("n/a (\\*)")
2413
- })),
2414
- ...unchanged.map((category) => ({
2415
- category: formatTitle(category),
2416
- after: formatScoreWithColor(category.score),
2417
- before: formatScoreWithColor(category.score, { skipBold: true }),
2418
- change: "\u2013"
2419
- }))
2420
- ].map(
2421
- (row) => hasChanges ? row : { category: row.category, before: row.before }
2422
- )
2423
- }),
2424
- added.length > 0 && section5(italicMd("(\\*) New category."))
2425
- );
2426
- }
2427
- function formatDiffGroupsSection(diff) {
2428
- if (diff.groups.changed.length + diff.groups.unchanged.length === 0) {
2429
- return "";
2430
- }
2431
- return lines5(
2432
- h24("\u{1F5C3}\uFE0F Groups"),
2433
- formatGroupsOrAuditsDetails("group", diff.groups, {
2434
- columns: [
2435
- { key: "plugin", label: "\u{1F50C} Plugin", align: "left" },
2436
- { key: "group", label: "\u{1F5C3}\uFE0F Group", align: "left" },
2437
- { key: "before", label: "\u2B50 Previous score" },
2438
- { key: "after", label: "\u2B50 Current score" },
2439
- { key: "change", label: "\u{1F504} Score change" }
2440
- ],
2441
- rows: sortChanges(diff.groups.changed).map((group) => ({
2442
- plugin: formatTitle(group.plugin),
2443
- group: formatTitle(group),
2444
- after: formatScoreWithColor(group.scores.after),
2445
- before: formatScoreWithColor(group.scores.before, { skipBold: true }),
2446
- change: formatScoreChange(group.scores.diff)
2447
- }))
2448
- })
2449
- );
2450
- }
2451
- function formatDiffAuditsSection(diff) {
2452
- return lines5(
2453
- h24("\u{1F6E1}\uFE0F Audits"),
2454
- formatGroupsOrAuditsDetails("audit", diff.audits, {
2455
- columns: [
2456
- { key: "plugin", label: "\u{1F50C} Plugin", align: "left" },
2457
- { key: "audit", label: "\u{1F6E1}\uFE0F Audit", align: "left" },
2458
- { key: "before", label: "\u{1F4CF} Previous value" },
2459
- { key: "after", label: "\u{1F4CF} Current value" },
2460
- { key: "change", label: "\u{1F504} Value change" }
2461
- ],
2462
- rows: sortChanges(diff.audits.changed).map((audit) => ({
2463
- plugin: formatTitle(audit.plugin),
2464
- audit: formatTitle(audit),
2465
- after: `${scoreMarker(audit.scores.after, "square")} ${boldMd3(
2466
- audit.displayValues.after || audit.values.after.toString()
2467
- )}`,
2468
- before: `${scoreMarker(audit.scores.before, "square")} ${audit.displayValues.before || audit.values.before.toString()}`,
2469
- change: formatValueChange(audit)
2470
- }))
2471
- })
2472
- );
2473
- }
2474
- function formatGroupsOrAuditsDetails(token, { changed, unchanged }, tableData) {
2475
- return changed.length === 0 ? summarizeUnchanged(token, { changed, unchanged }) : details3(
2476
- summarizeDiffOutcomes(changesToDiffOutcomes(changed), token),
2477
- lines5(
2478
- table4({
2479
- ...tableData,
2480
- rows: tableData.rows.slice(0, MAX_ROWS)
2481
- // use never to avoid typing problem
2482
- }),
2483
- changed.length > MAX_ROWS && italicMd(
2484
- `Only the ${MAX_ROWS} most affected ${pluralize(
2485
- token
2486
- )} are listed above for brevity.`
2487
- ),
2488
- unchanged.length > 0 && summarizeUnchanged(token, { changed, unchanged })
2489
- )
2490
- );
2491
- }
2492
- function formatScoreChange(diff) {
2493
- const marker = getDiffMarker(diff);
2494
- const text = formatDiffNumber(Math.round(diff * 1e3) / 10);
2495
- return colorByScoreDiff(`${marker} ${text}`, diff);
2496
- }
2497
- function formatValueChange({
2498
- values,
2499
- scores
2500
- }) {
2501
- const marker = getDiffMarker(values.diff);
2502
- const percentage = values.before === 0 ? values.diff > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY : Math.round(100 * values.diff / values.before);
2503
- const text = `${formatDiffNumber(percentage)}\u2009%`;
2504
- return colorByScoreDiff(`${marker} ${text}`, scores.diff);
2505
- }
2506
2276
  function summarizeUnchanged(token, { changed, unchanged }) {
2507
- return section5(
2508
- [
2509
- changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`,
2510
- unchanged.length === 1 ? "is" : "are",
2511
- "unchanged."
2512
- ].join(" ")
2513
- );
2277
+ const pluralizedCount = changed.length > 0 ? pluralizeToken(`other ${token}`, unchanged.length) : `All of ${pluralizeToken(token, unchanged.length)}`;
2278
+ const pluralizedVerb = unchanged.length === 1 ? "is" : "are";
2279
+ return `${pluralizedCount} ${pluralizedVerb} unchanged.`;
2514
2280
  }
2515
2281
  function summarizeDiffOutcomes(outcomes, token) {
2516
2282
  return objectToEntries(countDiffOutcomes(outcomes)).filter(
@@ -2530,20 +2296,46 @@ function summarizeDiffOutcomes(outcomes, token) {
2530
2296
  }
2531
2297
  }).join(", ");
2532
2298
  }
2299
+ function createGroupsOrAuditsDetails(token, { changed, unchanged }, ...[columns, rows]) {
2300
+ if (changed.length === 0) {
2301
+ return new MarkdownDocument4().paragraph(
2302
+ summarizeUnchanged(token, { changed, unchanged })
2303
+ );
2304
+ }
2305
+ return new MarkdownDocument4().table(columns, rows.slice(0, MAX_ROWS)).paragraph(
2306
+ changed.length > MAX_ROWS && md5.italic(
2307
+ `Only the ${MAX_ROWS} most affected ${pluralize(
2308
+ token
2309
+ )} are listed above for brevity.`
2310
+ )
2311
+ ).paragraph(
2312
+ unchanged.length > 0 && summarizeUnchanged(token, { changed, unchanged })
2313
+ );
2314
+ }
2533
2315
  function formatTitle({
2534
2316
  title,
2535
2317
  docsUrl
2536
2318
  }) {
2537
2319
  if (docsUrl) {
2538
- return link7(docsUrl, title);
2320
+ return md5.link(docsUrl, title);
2539
2321
  }
2540
2322
  return title;
2541
2323
  }
2324
+ function formatPortalLink(portalUrl) {
2325
+ return portalUrl && md5.link(portalUrl, "\u{1F575}\uFE0F See full comparison in Code PushUp portal \u{1F50D}");
2326
+ }
2542
2327
  function sortChanges(changes) {
2543
2328
  return [...changes].sort(
2544
2329
  (a, b) => Math.abs(b.scores.diff) - Math.abs(a.scores.diff) || Math.abs(b.values?.diff ?? 0) - Math.abs(a.values?.diff ?? 0)
2545
2330
  );
2546
2331
  }
2332
+ function getDiffChanges(diff) {
2333
+ return [
2334
+ ...diff.categories.changed,
2335
+ ...diff.groups.changed,
2336
+ ...diff.audits.changed
2337
+ ];
2338
+ }
2547
2339
  function changesToDiffOutcomes(changes) {
2548
2340
  return changes.map((change) => {
2549
2341
  if (change.scores.diff > 0) {
@@ -2578,9 +2370,218 @@ function countDiffOutcomes(outcomes) {
2578
2370
  unchanged: outcomes.filter((outcome) => outcome === "unchanged").length
2579
2371
  };
2580
2372
  }
2373
+ function formatReportOutcome(outcome, commits) {
2374
+ const outcomeTexts = {
2375
+ positive: md5`🥳 Code PushUp report has ${md5.bold("improved")}`,
2376
+ negative: md5`😟 Code PushUp report has ${md5.bold("regressed")}`,
2377
+ mixed: md5`🤨 Code PushUp report has both ${md5.bold(
2378
+ "improvements and regressions"
2379
+ )}`,
2380
+ unchanged: md5`😐 Code PushUp report is ${md5.bold("unchanged")}`
2381
+ };
2382
+ if (commits) {
2383
+ const commitsText = `compared target commit ${commits.after.hash} with source commit ${commits.before.hash}`;
2384
+ return md5`${outcomeTexts[outcome]} – ${commitsText}.`;
2385
+ }
2386
+ return md5`${outcomeTexts[outcome]}.`;
2387
+ }
2388
+ function compareDiffsBy(type, a, b) {
2389
+ return sumScoreChanges(b[type].changed) - sumScoreChanges(a[type].changed) || sumConfigChanges(b[type]) - sumConfigChanges(a[type]);
2390
+ }
2391
+ function sumScoreChanges(changes) {
2392
+ return changes.reduce(
2393
+ (acc, { scores }) => acc + Math.abs(scores.diff),
2394
+ 0
2395
+ );
2396
+ }
2397
+ function sumConfigChanges({
2398
+ added,
2399
+ removed
2400
+ }) {
2401
+ return added.length + removed.length;
2402
+ }
2403
+
2404
+ // packages/utils/src/lib/reports/generate-md-reports-diff.ts
2405
+ function generateMdReportsDiff(diff) {
2406
+ return new MarkdownDocument5().$concat(
2407
+ createDiffHeaderSection(diff),
2408
+ createDiffCategoriesSection(diff),
2409
+ createDiffDetailsSection(diff)
2410
+ ).toString();
2411
+ }
2412
+ function generateMdReportsDiffForMonorepo(diffs) {
2413
+ const diffsWithOutcomes = diffs.map((diff) => ({
2414
+ ...diff,
2415
+ outcome: mergeDiffOutcomes(changesToDiffOutcomes(getDiffChanges(diff)))
2416
+ })).sort(
2417
+ (a, b) => compareDiffsBy("categories", a, b) || compareDiffsBy("groups", a, b) || compareDiffsBy("audits", a, b) || a.label.localeCompare(b.label)
2418
+ );
2419
+ const unchanged = diffsWithOutcomes.filter(
2420
+ ({ outcome }) => outcome === "unchanged"
2421
+ );
2422
+ const changed = diffsWithOutcomes.filter((diff) => !unchanged.includes(diff));
2423
+ return new MarkdownDocument5().$concat(
2424
+ createDiffHeaderSection(diffs),
2425
+ ...changed.map(createDiffProjectSection)
2426
+ ).$if(
2427
+ unchanged.length > 0,
2428
+ (doc) => doc.rule().paragraph(summarizeUnchanged("project", { unchanged, changed }))
2429
+ ).toString();
2430
+ }
2431
+ function createDiffHeaderSection(diff) {
2432
+ const outcome = mergeDiffOutcomes(
2433
+ changesToDiffOutcomes(toArray(diff).flatMap(getDiffChanges))
2434
+ );
2435
+ const commits = Array.isArray(diff) ? diff[0]?.commits : diff.commits;
2436
+ const portalUrl = Array.isArray(diff) ? void 0 : diff.portalUrl;
2437
+ return new MarkdownDocument5().heading(HIERARCHY.level_1, "Code PushUp").paragraph(formatReportOutcome(outcome, commits)).paragraph(formatPortalLink(portalUrl));
2438
+ }
2439
+ function createDiffProjectSection(diff) {
2440
+ return new MarkdownDocument5().heading(HIERARCHY.level_2, md6`💼 Project ${md6.code(diff.label)}`).paragraph(formatReportOutcome(diff.outcome)).paragraph(formatPortalLink(diff.portalUrl)).$concat(
2441
+ createDiffCategoriesSection(diff, {
2442
+ skipHeading: true,
2443
+ skipUnchanged: true
2444
+ }),
2445
+ createDiffDetailsSection(diff, HIERARCHY.level_3)
2446
+ );
2447
+ }
2448
+ function createDiffCategoriesSection(diff, options) {
2449
+ const { changed, unchanged, added } = diff.categories;
2450
+ const { skipHeading, skipUnchanged } = options ?? {};
2451
+ const categoriesCount = changed.length + unchanged.length + added.length;
2452
+ const hasChanges = unchanged.length < categoriesCount;
2453
+ if (categoriesCount === 0) {
2454
+ return null;
2455
+ }
2456
+ const [columns, rows] = createCategoriesTable(diff, {
2457
+ hasChanges,
2458
+ skipUnchanged
2459
+ });
2460
+ return new MarkdownDocument5().heading(HIERARCHY.level_2, !skipHeading && "\u{1F3F7}\uFE0F Categories").table(columns, rows).paragraph(added.length > 0 && md6.italic("(\\*) New category.")).paragraph(
2461
+ skipUnchanged && unchanged.length > 0 && summarizeUnchanged("category", { changed, unchanged })
2462
+ );
2463
+ }
2464
+ function createCategoriesTable(diff, options) {
2465
+ const { changed, unchanged, added } = diff.categories;
2466
+ const { hasChanges, skipUnchanged } = options;
2467
+ const columns = [
2468
+ { heading: "\u{1F3F7}\uFE0F Category", alignment: "left" },
2469
+ {
2470
+ heading: hasChanges ? "\u2B50 Previous score" : "\u2B50 Score",
2471
+ alignment: "center"
2472
+ },
2473
+ { heading: "\u2B50 Current score", alignment: "center" },
2474
+ { heading: "\u{1F504} Score change", alignment: "center" }
2475
+ ];
2476
+ const rows = [
2477
+ ...sortChanges(changed).map((category) => [
2478
+ formatTitle(category),
2479
+ formatScoreWithColor(category.scores.before, {
2480
+ skipBold: true
2481
+ }),
2482
+ formatScoreWithColor(category.scores.after),
2483
+ formatScoreChange(category.scores.diff)
2484
+ ]),
2485
+ ...added.map((category) => [
2486
+ formatTitle(category),
2487
+ md6.italic("n/a (\\*)"),
2488
+ formatScoreWithColor(category.score),
2489
+ md6.italic("n/a (\\*)")
2490
+ ]),
2491
+ ...skipUnchanged ? [] : unchanged.map((category) => [
2492
+ formatTitle(category),
2493
+ formatScoreWithColor(category.score, { skipBold: true }),
2494
+ formatScoreWithColor(category.score),
2495
+ "\u2013"
2496
+ ])
2497
+ ];
2498
+ return [
2499
+ hasChanges ? columns : columns.slice(0, 2),
2500
+ rows.map((row) => hasChanges ? row : row.slice(0, 2))
2501
+ ];
2502
+ }
2503
+ function createDiffDetailsSection(diff, level = HIERARCHY.level_2) {
2504
+ if (diff.groups.changed.length + diff.audits.changed.length === 0) {
2505
+ return null;
2506
+ }
2507
+ const summary = ["group", "audit"].map(
2508
+ (token) => summarizeDiffOutcomes(
2509
+ changesToDiffOutcomes(diff[`${token}s`].changed),
2510
+ token
2511
+ )
2512
+ ).filter(Boolean).join(", ");
2513
+ const details2 = new MarkdownDocument5().$concat(
2514
+ createDiffGroupsSection(diff, level),
2515
+ createDiffAuditsSection(diff, level)
2516
+ );
2517
+ return new MarkdownDocument5().details(summary, details2);
2518
+ }
2519
+ function createDiffGroupsSection(diff, level) {
2520
+ if (diff.groups.changed.length + diff.groups.unchanged.length === 0) {
2521
+ return null;
2522
+ }
2523
+ return new MarkdownDocument5().heading(level, "\u{1F5C3}\uFE0F Groups").$concat(
2524
+ createGroupsOrAuditsDetails(
2525
+ "group",
2526
+ diff.groups,
2527
+ [
2528
+ { heading: "\u{1F50C} Plugin", alignment: "left" },
2529
+ { heading: "\u{1F5C3}\uFE0F Group", alignment: "left" },
2530
+ { heading: "\u2B50 Previous score", alignment: "center" },
2531
+ { heading: "\u2B50 Current score", alignment: "center" },
2532
+ { heading: "\u{1F504} Score change", alignment: "center" }
2533
+ ],
2534
+ sortChanges(diff.groups.changed).map((group) => [
2535
+ formatTitle(group.plugin),
2536
+ formatTitle(group),
2537
+ formatScoreWithColor(group.scores.before, { skipBold: true }),
2538
+ formatScoreWithColor(group.scores.after),
2539
+ formatScoreChange(group.scores.diff)
2540
+ ])
2541
+ )
2542
+ );
2543
+ }
2544
+ function createDiffAuditsSection(diff, level) {
2545
+ return new MarkdownDocument5().heading(level, "\u{1F6E1}\uFE0F Audits").$concat(
2546
+ createGroupsOrAuditsDetails(
2547
+ "audit",
2548
+ diff.audits,
2549
+ [
2550
+ { heading: "\u{1F50C} Plugin", alignment: "left" },
2551
+ { heading: "\u{1F6E1}\uFE0F Audit", alignment: "left" },
2552
+ { heading: "\u{1F4CF} Previous value", alignment: "center" },
2553
+ { heading: "\u{1F4CF} Current value", alignment: "center" },
2554
+ { heading: "\u{1F504} Value change", alignment: "center" }
2555
+ ],
2556
+ sortChanges(diff.audits.changed).map((audit) => [
2557
+ formatTitle(audit.plugin),
2558
+ formatTitle(audit),
2559
+ `${scoreMarker(audit.scores.before, "square")} ${audit.displayValues.before || audit.values.before.toString()}`,
2560
+ md6`${scoreMarker(audit.scores.after, "square")} ${md6.bold(
2561
+ audit.displayValues.after || audit.values.after.toString()
2562
+ )}`,
2563
+ formatValueChange(audit)
2564
+ ])
2565
+ )
2566
+ );
2567
+ }
2568
+
2569
+ // packages/utils/src/lib/reports/load-report.ts
2570
+ import { join as join3 } from "node:path";
2571
+ async function loadReport(options) {
2572
+ const { outputDir, filename, format } = options;
2573
+ await ensureDirectoryExists(outputDir);
2574
+ const filePath = join3(outputDir, `${filename}.${format}`);
2575
+ if (format === "json") {
2576
+ const content = await readJsonFile(filePath);
2577
+ return reportSchema.parse(content);
2578
+ }
2579
+ const text = await readTextFile(filePath);
2580
+ return text;
2581
+ }
2581
2582
 
2582
2583
  // packages/utils/src/lib/reports/log-stdout-summary.ts
2583
- import chalk4 from "chalk";
2584
+ import { bold as bold4, cyan, cyanBright, green as green2, red } from "ansis";
2584
2585
  function log(msg = "") {
2585
2586
  ui().logger.log(msg);
2586
2587
  }
@@ -2597,14 +2598,14 @@ function logStdoutSummary(report) {
2597
2598
  }
2598
2599
  function reportToHeaderSection(report) {
2599
2600
  const { packageName, version } = report;
2600
- return `${chalk4.bold(reportHeadlineText)} - ${packageName}@${version}`;
2601
+ return `${bold4(REPORT_HEADLINE_TEXT)} - ${packageName}@${version}`;
2601
2602
  }
2602
2603
  function logPlugins(report) {
2603
2604
  const { plugins } = report;
2604
2605
  plugins.forEach((plugin) => {
2605
2606
  const { title, audits } = plugin;
2606
2607
  log();
2607
- log(chalk4.magentaBright.bold(`${title} audits`));
2608
+ log(bold4.magentaBright(`${title} audits`));
2608
2609
  log();
2609
2610
  audits.forEach((audit) => {
2610
2611
  ui().row([
@@ -2619,8 +2620,9 @@ function logPlugins(report) {
2619
2620
  padding: [0, 3, 0, 0]
2620
2621
  },
2621
2622
  {
2622
- text: chalk4.cyanBright(audit.displayValue || `${audit.value}`),
2623
- width: 10,
2623
+ text: cyanBright(audit.displayValue || `${audit.value}`),
2624
+ // eslint-disable-next-line no-magic-numbers
2625
+ width: 20,
2624
2626
  padding: [0, 0, 0, 0]
2625
2627
  }
2626
2628
  ]);
@@ -2630,42 +2632,38 @@ function logPlugins(report) {
2630
2632
  }
2631
2633
  function logCategories({ categories, plugins }) {
2632
2634
  const hAlign = (idx) => idx === 0 ? "left" : "right";
2633
- const rows = categories.map(({ title, score, refs }) => [
2635
+ const rows = categories.map(({ title, score, refs, isBinary }) => [
2634
2636
  title,
2635
- applyScoreColor({ score }),
2637
+ `${binaryIconPrefix(score, isBinary)}${applyScoreColor({ score })}`,
2636
2638
  countCategoryAudits(refs, plugins)
2637
2639
  ]);
2638
- const table5 = ui().table();
2639
- table5.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]);
2640
- table5.head(
2641
- reportRawOverviewTableHeaders.map((heading, idx) => ({
2642
- content: chalk4.cyan(heading),
2640
+ const table2 = ui().table();
2641
+ table2.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]);
2642
+ table2.head(
2643
+ REPORT_RAW_OVERVIEW_TABLE_HEADERS.map((heading, idx) => ({
2644
+ content: cyan(heading),
2643
2645
  hAlign: hAlign(idx)
2644
2646
  }))
2645
2647
  );
2646
2648
  rows.forEach(
2647
- (row) => table5.row(
2649
+ (row) => table2.row(
2648
2650
  row.map((content, idx) => ({
2649
2651
  content: content.toString(),
2650
2652
  hAlign: hAlign(idx)
2651
2653
  }))
2652
2654
  )
2653
2655
  );
2654
- log(chalk4.magentaBright.bold("Categories"));
2656
+ log(bold4.magentaBright("Categories"));
2655
2657
  log();
2656
- table5.render();
2658
+ table2.render();
2657
2659
  log();
2658
2660
  }
2659
- function applyScoreColor({ score, text }) {
2660
- const formattedScore = text ?? formatReportScore(score);
2661
- const style = text ? chalk4 : chalk4.bold;
2662
- if (score >= SCORE_COLOR_RANGE.GREEN_MIN) {
2663
- return style.green(formattedScore);
2664
- }
2665
- if (score >= SCORE_COLOR_RANGE.YELLOW_MIN) {
2666
- return style.yellow(formattedScore);
2667
- }
2668
- return style.red(formattedScore);
2661
+ function binaryIconPrefix(score, isBinary) {
2662
+ return targetScoreIcon(score, isBinary ? 1 : void 0, {
2663
+ passIcon: bold4(green2("\u2713")),
2664
+ failIcon: bold4(red("\u2717")),
2665
+ postfix: " "
2666
+ });
2669
2667
  }
2670
2668
 
2671
2669
  // packages/utils/src/lib/reports/scoring.ts
@@ -2755,56 +2753,6 @@ function parseScoringParameters(refs, scoreFn) {
2755
2753
  return scoredRefs;
2756
2754
  }
2757
2755
 
2758
- // packages/utils/src/lib/reports/sorting.ts
2759
- function sortReport(report) {
2760
- const { categories, plugins } = report;
2761
- const sortedCategories = categories.map((category) => {
2762
- const { audits, groups } = category.refs.reduce(
2763
- (acc, ref) => ({
2764
- ...acc,
2765
- ...ref.type === "group" ? {
2766
- groups: [...acc.groups, getSortableGroupByRef(ref, plugins)]
2767
- } : {
2768
- audits: [...acc.audits, getSortableAuditByRef(ref, plugins)]
2769
- }
2770
- }),
2771
- { groups: [], audits: [] }
2772
- );
2773
- const sortedAuditsAndGroups = [...audits, ...groups].sort(
2774
- compareCategoryAuditsAndGroups
2775
- );
2776
- const sortedRefs = [...category.refs].sort((a, b) => {
2777
- const aIndex = sortedAuditsAndGroups.findIndex(
2778
- (ref) => ref.slug === a.slug && ref.plugin === a.plugin
2779
- );
2780
- const bIndex = sortedAuditsAndGroups.findIndex(
2781
- (ref) => ref.slug === b.slug && ref.plugin === b.plugin
2782
- );
2783
- return aIndex - bIndex;
2784
- });
2785
- return { ...category, refs: sortedRefs };
2786
- });
2787
- return {
2788
- ...report,
2789
- categories: sortedCategories,
2790
- plugins: sortPlugins(plugins)
2791
- };
2792
- }
2793
- function sortPlugins(plugins) {
2794
- return plugins.map((plugin) => ({
2795
- ...plugin,
2796
- audits: [...plugin.audits].sort(compareAudits).map(
2797
- (audit) => audit.details?.issues ? {
2798
- ...audit,
2799
- details: {
2800
- ...audit.details,
2801
- issues: [...audit.details.issues].sort(compareIssues)
2802
- }
2803
- } : audit
2804
- )
2805
- }));
2806
- }
2807
-
2808
2756
  // packages/utils/src/lib/verbose-utils.ts
2809
2757
  function getLogVerbose(verbose = false) {
2810
2758
  return (msg) => {
@@ -2827,13 +2775,13 @@ var verboseUtils = (verbose = false) => ({
2827
2775
  export {
2828
2776
  CODE_PUSHUP_DOMAIN,
2829
2777
  FOOTER_PREFIX,
2778
+ HIERARCHY,
2830
2779
  NEW_LINE,
2831
2780
  ProcessError,
2832
2781
  README_LINK,
2833
2782
  SPACE,
2834
2783
  TAB,
2835
2784
  TERMINAL_WIDTH,
2836
- apostrophize,
2837
2785
  calcDuration,
2838
2786
  capitalize,
2839
2787
  compareIssueSeverity,
@@ -2853,9 +2801,11 @@ export {
2853
2801
  formatBytes,
2854
2802
  formatDuration,
2855
2803
  formatGitPath,
2804
+ formatReportScore,
2856
2805
  fromJsonLines,
2857
2806
  generateMdReport,
2858
2807
  generateMdReportsDiff,
2808
+ generateMdReportsDiffForMonorepo,
2859
2809
  getCurrentBranchOrTag,
2860
2810
  getGitRoot,
2861
2811
  getHashFromTag,
@@ -2878,7 +2828,6 @@ export {
2878
2828
  logMultipleResults,
2879
2829
  logStdoutSummary,
2880
2830
  matchArrayItemsByKey,
2881
- md,
2882
2831
  mergeConfigs,
2883
2832
  normalizeSemver,
2884
2833
  objectFromEntries,
@@ -2896,6 +2845,7 @@ export {
2896
2845
  slugify,
2897
2846
  sortReport,
2898
2847
  sortSemvers,
2848
+ stringifyError,
2899
2849
  toArray,
2900
2850
  toGitPath,
2901
2851
  toJsonLines,